빠른 메시지 큐(FMQ)

AIDL 지원이 필요한 경우 AIDL과 함께 사용하는 FMQ도 참고하세요.

HIDL의 리모트 프로시져 콜(RPC) 인프라는 바인더 메커니즘을 사용합니다. 즉, 호출에 오버헤드가 포함되고 커널 작업이 필요하며 스케줄러 작업이 트리거될 수 있습니다. 그러나 프로세스 간에 데이터가 적은 오버헤드로 커널 간섭 없이 전송되어야 하는 경우 빠른 메시지 큐(FMQ) 시스템이 사용됩니다.

FMQ를 사용하면 원하는 속성이 적용된 메시지 큐를 만들 수 있습니다. MQDescriptorSync 또는 MQDescriptorUnsync 객체는 HIDL RPC 호출을 통해 전송될 수 있으며 수신 프로세스에서 메시지 큐에 액세스하는 데 사용될 수 있습니다.

빠른 메시지 큐는 C ++ 및 Android 8.0 이상을 실행하는 기기에서만 지원됩니다.

MessageQueue 유형

Android에서는 두 가지 큐 유형(버전이라고 함)을 지원합니다.

  • 비동기식 큐는 오버플로가 허용되며 많은 판독기가 포함될 수 있습니다. 각 판독기는 적시에 데이터를 읽어야 하며 그러지 않으면 데이터를 잃습니다.
  • 동기식 큐는 오버플로가 허용되지 않으며 하나의 판독기만 포함될 수 있습니다.

두 큐 유형 모두 언더플로가 허용되지 않아 빈 큐에서는 읽기에 실패하게 되며 하나의 작성기만 포함될 수 있습니다.

비동기식

비동기식 큐에는 하나의 작성기만 포함되지만, 여러 판독기가 포함될 수 있습니다. 큐에는 쓰기 위치가 하나만 있습니다. 하지만 각 판독기는 자체적인 별도의 읽기 위치를 추적합니다.

큐에 쓰기 데이터가 구성된 큐 용량보다 크지 않는 한 항상 성공(오버플로로 확인되지 않음)하며, 쓰기 데이터가 큐 용량보다 큰 경우 즉시 실패합니다. 각 판독기마다 읽기 위치가 다를 수 있으므로 각 읽기를 기다리지 않고 새 쓰기 공간이 필요할 때마다 데이터가 큐에서 빠질 수 있습니다.

판독기는 큐의 끝에서 데이터가 빠지기 전에 데이터를 검색해야 합니다. 처리할 수 있는 데이터보다 더 많은 데이터를 읽으려는 읽기 작업은 즉시 실패하거나(비블로킹의 경우) 충분한 데이터가 모일 때까지 보류됩니다(블로킹의 경우). 큐 용량보다 큰 데이터를 읽으려는 읽기 작업은 즉시 실패합니다.

판독기가 작성기를 따라잡지 못하여 판독기가 아직 읽지 않은 데이터의 양이 큐 용량보다 크면 다음 읽기 시 데이터를 반환하지 않습니다. 대신에 판독기의 읽기 위치를 마지막 쓰기 위치와 같도록 재설정한 후 실패를 반환합니다. 읽을 수 있는 데이터가 오버플로 이후, 다음 읽기 전에 확인되는 경우는 큐 용량보다 읽을 수 있는 데이터가 더 많으며 오버플로가 발생했음을 나타냅니다. 처리할 수 있는 데이터를 확인하고 해당 데이터를 읽는 동안 큐에서 오버플로가 발생하는 경우는 오버플로 발생 사실을 나타내는 신호는 읽기 실패뿐입니다.

비동기식 큐의 판독기는 큐의 읽기 및 쓰기 포인터를 재설정하지 않으려고 할 가능성이 높습니다. 따라서 설명어에서 큐를 만들 때 판독기는 `resetPointers` 매개변수에 `false` 인수를 사용해야 합니다.

동기식

동기식 큐에는 작성기가 1개, 판독기가 1개, 쓰기 위치가 1개, 읽기 위치가 1개 있습니다. 큐에 있는 공간보다 더 큰 데이터를 쓰거나 현재 큐에 있는 것보다 더 많은 데이터를 읽을 수 없습니다. 호출된 함수가 블로킹 또는 비블로킹이냐, 쓰기 또는 읽기 함수이냐에 따라 사용 가능한 공간 또는 데이터를 초과하면 즉시 실패하거나 원하는 작업을 완료할 수 있을 때까지 차단됩니다. 큐 용량보다 더 큰 데이터를 읽거나 쓰려고 시도하면 항상 즉시 실패합니다.

FMQ 설정

메시지 큐에는 여러 MessageQueue 객체가 필요합니다. 하나는 쓰는 데, 나머지는 읽는 데 씁니다. 어떤 객체가 쓰기 또는 읽기에 사용되는지에 관한 명확한 구성 방침은 없습니다. 한 객체를 읽기와 쓰기 모두에 사용하지 않고, 최대 하나의 작성기를 사용하고, 동기식 큐의 경우 최대 하나의 판독기를 사용하도록 하는 방식은 사용자에 따라 다릅니다.

첫 번째 MessageQueue 객체 생성

메시지 큐는 단일 호출로 생성 및 구성됩니다.

#include <fmq/MessageQueue.h>
using android::hardware::kSynchronizedReadWrite;
using android::hardware::kUnsynchronizedWrite;
using android::hardware::MQDescriptorSync;
using android::hardware::MQDescriptorUnsync;
using android::hardware::MessageQueue;
....
// For a synchronized non-blocking FMQ
mFmqSynchronized =
  new (std::nothrow) MessageQueue<uint16_t, kSynchronizedReadWrite>
      (kNumElementsInQueue);
// For an unsynchronized FMQ that supports blocking
mFmqUnsynchronizedBlocking =
  new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
      (kNumElementsInQueue, true /* enable blocking operations */);
  • MessageQueue<T, flavor>(numElements) 이니셜라이저는 메시지 큐 기능을 지원하는 객체를 생성하고 초기화합니다.
  • MessageQueue<T, flavor>(numElements, configureEventFlagWord) 이니셜라이저는 블로킹식 메시지 큐 기능을 지원하는 객체를 생성하고 초기화합니다.
  • flavor는 동기식 큐의 경우 kSynchronizedReadWrite, 비동기식 큐의 경우 kUnsynchronizedWrite일 수 있습니다.
  • 이 예시의 uint16_t는 중첩된 버퍼(string 또는 vec 형식이 아닌 형식), 핸들 또는 인터페이스를 수반하지 않는 HIDL 정의 유형일 수 있습니다.
  • kNumElementsInQueue는 항목 수로 큐의 크기를 나타내며 큐에 할당될 공유 메모리 버퍼의 크기를 결정합니다.

두 번째 MessageQueue 객체 생성

첫 번째 끝에서 획득한 MQDescriptor 객체를 사용하면 메시지 큐의 두 번째 끝이 생성됩니다. MQDescriptor 객체는 HIDL 또는 AIDL RPC 호출을 통해 메시지 큐의 두 번째 끝을 포함하는 프로세스로 전송됩니다. MQDescriptor에는 큐에 관한 다음과 같은 정보가 포함됩니다.

  • 버퍼 및 쓰기 포인터를 매핑하는 정보
  • 읽기 포인터를 매핑하기 위한 정보(동기식 큐인 경우)
  • 이벤트 플래그 단어를 매핑하는 정보(블로킹 큐인 경우)
  • 객체 유형(<T, flavor>)으로 큐 요소의 HIDL 정의 유형과 큐 버전(동기식 또는 비동기식)이 포함됩니다.

MQDescriptor 객체를 사용하여 MessageQueue 객체를 구성할 수 있습니다.

MessageQueue<T, flavor>::MessageQueue(const MQDescriptor<T, flavor>& Desc, bool resetPointers)

resetPointers 매개변수는 이 MessageQueue 객체를 생성하는 도중에 읽기 및 쓰기 위치를 0으로 재설정할지 여부를 나타냅니다. 비동기식 큐에서 읽기 위치(비동기식 큐의 각 MessageQueue 객체의 로컬)는 생성 중에 항상 0으로 설정됩니다. 일반적으로 MQDescriptor는 첫 번째 메시지 큐 객체를 생성하는 동안 초기화됩니다. 공유 메모리를 추가로 제어하기 위해 MQDescriptor를 수동으로 설정(MQDescriptorsystem/libhidl/base/include/hidl/MQDescriptor.h에 정의되어 있음)할 수 있으며, 그런 다음 이 섹션에 정의된 대로 모든 MessageQueue 객체를 생성할 수 있습니다.

블로킹 큐 및 이벤트 플래그

기본적으로 큐는 블로킹 읽기 및 쓰기를 지원하지 않습니다. 블로킹 읽기 및 쓰기 호출에는 두 가지 형식이 있습니다.

  • 짧은 형식에는 매개변수 3개(데이터 포인터, 항목 수, 시간 제한)가 포함됩니다. 단일 큐에서의 개별 읽기 및 쓰기 작업의 블로킹을 지원합니다. 이 형식을 사용하는 경우 큐가 내부적으로 이벤트 플래그와 비트마스크를 처리하고 첫 번째 메시지 큐 객체true의 두 번째 매개변수로 초기화되어야 합니다. 예:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • 긴 형식에는 매개변수 6개(이벤트 플래그 및 비트마스크 포함)가 포함됩니다. 여러 큐에서 공유된 EventFlag 객체의 사용을 지원하며 사용될 알림 비트마스크를 지정할 수 있습니다. 이러한 경우 각 읽기 및 쓰기 호출에 이벤트 플래그와 비트마스크를 제공해야 합니다.

긴 형식의 경우 EventFlag를 각 readBlocking()writeBlocking() 호출에 명시적으로 제공할 수 있습니다. 큐 중 하나는 내부 이벤트 플래그를 통해 초기화될 수 있습니다. 내부 이벤트 플래그는 getEventFlagWord()를 사용하여 해당 큐의 MessageQueue 객체에서 추출되어야 하고, 다른 FMQ와 함께 사용할 수 있도록 각 프로세스의 EventFlag 객체를 생성하는 데 사용되어야 합니다. 또는 EventFlag 객체를 적합한 공유 메모리를 통해 초기화할 수 있습니다.

일반적으로 각 큐는 비블로킹, 짧은 형식 블로킹, 긴 형식 블로킹 중 하나만 사용해야 합니다. 이를 혼합하는 것이 잘못된 방법은 아니지만, 원하는 결과를 얻으려면 신중하게 프로그래밍해야 합니다.

메모리를 읽기 전용으로 표시

기본적으로 공유 메모리에는 읽기 및 쓰기 권한이 있습니다. 동기화되지 않은 대기열(kUnsynchronizedWrite)의 경우 작성자는 MQDescriptorUnsync 객체를 전달하기 전에 모든 판독기의 쓰기 권한을 삭제하려고 할 수 있습니다. 이렇게 하면 다른 프로세스에서 대기열에 쓸 수 없으며 이는 판독기 프로세스에서 버그나 비정상적인 동작을 방지하는 데 권장됩니다. 작성자가 MQDescriptorUnsync를 사용하여 읽기 측의 대기열을 만들 때마다 판독기가 대기열을 재설정할 수 있도록 하려는 경우 메모리를 읽기 전용으로 표시할 수 없습니다. 이는 `MessageQueue` 생성자의 기본 동작입니다. 따라서 이미 이 대기열의 기존 사용자가 있다면 resetPointer=false를 사용하여 대기열을 구성하도록 코드를 변경해야 합니다.

  • 작성자: MQDescriptor 파일 설명자와 읽기 전용으로 설정된 리전(PROT_READ)을 사용하여 ashmem_set_prot_region을 호출합니다.
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • 판독기: resetPointer=false(기본값은 true)를 사용하여 메시지 대기열을 만듭니다.
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

MessageQueue 사용

MessageQueue 객체의 공개 API는 다음과 같습니다.

size_t availableToWrite()  // Space available (number of elements).
size_t availableToRead()  // Number of elements available.
size_t getQuantumSize()  // Size of type T in bytes.
size_t getQuantumCount() // Number of items of type T that fit in the FMQ.
bool isValid() // Whether the FMQ is configured correctly.
const MQDescriptor<T, flavor>* getDesc()  // Return info to send to other process.

bool write(const T* data)  // Write one T to FMQ; true if successful.
bool write(const T* data, size_t count) // Write count T's; no partial writes.

bool read(T* data);  // read one T from FMQ; true if successful.
bool read(T* data, size_t count);  // Read count T's; no partial reads.

bool writeBlocking(const T* data, size_t count, int64_t timeOutNanos = 0);
bool readBlocking(T* data, size_t count, int64_t timeOutNanos = 0);

// Allows multiple queues to share a single event flag word
std::atomic<uint32_t>* getEventFlagWord();

bool writeBlocking(const T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr); // Blocking write operation for count Ts.

bool readBlocking(T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr) // Blocking read operation for count Ts;

//APIs to allow zero copy read/write operations
bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);
bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);

availableToWrite()availableToRead()는 단일 작업에서 전송할 수 있는 데이터의 양을 결정하는 데 사용할 수 있습니다. 비동기식 큐는 다음과 같은 특징이 있습니다.

  • availableToWrite()가 항상 큐의 용량을 반환합니다.
  • 각 판독기는 자체적인 읽기 위치를 사용하며 availableToRead()를 자체적으로 계산합니다.
  • 느린 판독기의 관점에서 큐는 오버플로될 수 있습니다. 이로 인해 availableToRead()가 큐의 크기보다 큰 값을 반환할 수도 있습니다. 오버플로 이후 첫 번째 읽기가 실패하면 availableToRead()를 통해 오버플로가 보고되었는지 여부와 상관없이 현재 쓰기 포인터에 읽기 위치가 설정됩니다.

요청한 모든 데이터의 송수신이 큐에서 이루어질 수 있거나 이루어진 경우 read()write() 메서드는 true를 반환합니다. 이러한 메서드는 블로킹 방식이 아닙니다. 즉, 성공하여 true를 반환하거나 실패(false)를 즉시 반환합니다.

readBlocking()writeBlocking() 메서드는 요청한 작업이 완료될 수 있을 때까지 또는 제한 시간이 초과될 때까지 대기합니다. timeOutNanos 값에서 0은 시간 제한이 없음을 의미합니다.

차단 작업은 이벤트 플래그 단어를 사용하여 실행됩니다. 기본적으로 각 큐는 자체적인 플래그 단어를 생성하고 이를 사용하여 readBlocking()writeBlocking()의 짧은 형식을 지원합니다. 여러 큐가 한 단어를 공유하여 프로세스가 모든 큐의 쓰기 또는 읽기를 기다릴 수 있습니다. 큐의 이벤트 플래그 단어의 포인터는 getEventFlagWord()를 호출하여 획득할 수 있고, 해당 포인터(또는 적합한 공유 메모리 위치의 포인터)를 사용하여 EventFlag 객체를 생성함으로써 다른 큐의 readBlocking()writeBlocking()의 긴 형식으로 전달할 수 있습니다. readNotificationwriteNotification 매개변수는 이벤트 플래그에서 해당 큐의 읽기 및 쓰기 신호를 보내는 데 사용되어야 하는 비트를 지정합니다. readNotificationwriteNotification은 32비트 비트마스크입니다.

readBlocking()writeNotification 비트를 기다립니다. 해당 매개변수가 0이면 호출은 항상 실패합니다. readNotification 값이 0이면 호출이 실패하지는 않지만, 읽기에 성공하더라도 알림 비트가 설정되지 않습니다. 이는 동기식 큐에서 비트가 다른 곳에 설정되어 있지 않으면 해당하는 writeBlocking() 호출이 대기 상태를 해제하지 않는다는 것을 의미합니다. 비동기식 큐에서 writeBlocking()은 기다리지 않으며(계속 쓰기 알림 비트를 설정하는 데 사용되어야 함), 읽기에 알림 비트를 설정하지 않는 것이 좋습니다. 이와 마찬가지로, writeblocking()readNotification이 0이면 실패하며 쓰기에 성공하면 지정된 writeNotification 비트를 설정합니다.

한 번에 여러 큐를 기다리려면 EventFlag 객체의 wait() 메서드를 호출하여 알림의 비트마스크를 기다립니다. wait() 메서드는 대기 상태를 해제한 비트가 포함된 상태 단어를 반환합니다. 그런 다음 이 정보는 해당 큐에 원하는 쓰기/읽기 작업을 위한 충분한 공간 또는 데이터가 있는지 확인하고 비블로킹 write()/read()를 실행하는 데 사용됩니다. 게시 작업 알림을 받으려면 EventFlagwake() 메서드의 다른 호출을 사용합니다. EventFlag 추상화의 정의는 system/libfmq/include/fmq/EventFlag.h를 참조하세요.

제로 카피(zero copy) 작업

read/write/readBlocking/writeBlocking() API는 입출력 버퍼의 포인터를 인수로 사용하고 memcpy() 호출을 내부적으로 사용하여 동일한 버퍼 및 FMQ 링 버퍼 간에 데이터를 복사합니다. 성능을 개선하기 위해 Android 8.0 이상에는 링 버퍼에 직접 포인터 액세스를 제공하는 일련의 API가 포함되어 memcpy 호출을 사용할 필요가 없습니다.

제로 카피 FMQ 작업에 다음 공개 API를 사용합니다.

bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);

bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);
  • beginWrite 메서드는 FMQ 링 버퍼에 기본 포인터를 제공합니다. 데이터가 기록되고 나면 commitWrite()를 사용하여 커밋합니다. beginRead/commitRead 메서드는 동일한 방식으로 작동합니다.
  • beginRead/Write 메서드는 입력으로 읽거나 쓸 메시지 수를 가져오고 읽기/쓰기 가능 여부를 나타내는 부울 값을 반환합니다. 읽기 또는 쓰기가 가능한 경우 memTx 구조체는 링 버퍼 공유 메모리로의 직접 포인터 액세스에 사용될 수 있는 기본 포인터로 채워집니다.
  • MemRegion 구조체는 기본 포인터(메모리 블록의 기본 주소)와 T의 길이(HIDL 정의 메시지 큐 유형의 메모리 블록 길이) 등 메모리 블록에 관한 세부정보를 포함합니다.
  • 링 버퍼로 읽기 또는 쓰기가 큐 시작 주변의 래핑을 필요로 할 수 있으므로 MemTransaction 구조체에는 2개의 MemRegion 구조체, firstsecond가 포함됩니다. 이는 FMQ 링 버퍼로 데이터를 읽고 쓰는 데 두 개의 기본 포인터가 필요하다는 것을 의미합니다.

기본 주소와 길이를 MemRegion 구조체로부터 가져오기:

T* getAddress(); // gets the base address
size_t getLength(); // gets the length of the memory region in terms of T
size_t getLengthInBytes(); // gets the length of the memory region in bytes

MemTransaction 객체 내 첫 번째와 두 번째 MemRegion의 참조 가져오기:

const MemRegion& getFirstRegion(); // get a reference to the first MemRegion
const MemRegion& getSecondRegion(); // get a reference to the second MemRegion

제로 카피 API를 사용한 FMQ 쓰기의 예시:

MessageQueueSync::MemTransaction tx;
if (mQueue->beginRead(dataLen, &tx)) {
    auto first = tx.getFirstRegion();
    auto second = tx.getSecondRegion();

    foo(first.getAddress(), first.getLength()); // method that performs the data write
    foo(second.getAddress(), second.getLength()); // method that performs the data write

    if(commitWrite(dataLen) == false) {
       // report error
    }
} else {
   // report error
}

또한 다음 도우미 메서드는 MemTransaction의 일부입니다.

  • T* getSlot(size_t idx);
    MemTransaction 객체의 일부인 MemRegions 내의 슬롯 idx의 포인터를 반환합니다. MemTransaction 객체가 T 형식의 N개 항목을 읽고 쓰는 메모리 영역을 나타내는 경우 idx의 유효한 범위는 0에서 N-1 사이입니다.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    startIdx 색인부터 T 형식의 nMessages개 항목을 객체에 명시된 메모리 영역에 씁니다. 이 메서드는 memcpy()를 사용하며 제로 카피 작업에 사용되지 않습니다. MemTransaction 객체가 T 형식의 N개 항목을 읽고 쓰는 메모리를 나타내는 경우 idx의 유효한 범위는 0에서 N-1 사이입니다.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    도우미 메서드로 객체에 명시된 메모리 영역에서 startIdx부터 T 형식의 nMessages개 항목을 읽습니다. 이 메서드는 memcpy()를 사용하며 제로 카피 작업에 사용되지 않습니다.

HIDL을 통한 큐 전송

생성 측면:

  1. 위에서 설명한 대로 메시지 큐 객체를 생성합니다.
  2. isValid()로 객체가 유효한지 검증합니다.
  3. EventFlagreadBlocking()/writeBlocking()의 긴 형식으로 전달하여 여러 큐를 기다리는 경우 getEventFlagWord()를 사용하여 초기화를 통해 플래그를 생성하는 MessageQueue 객체로부터 이벤트 플래그 포인터를 추출하고, 이 플래그를 사용하여 필요한 EventFlag 객체를 생성할 수 있습니다.
  4. MessageQueue getDesc() 메서드를 사용하여 설명자 객체를 가져옵니다.
  5. .hal 파일에 T가 적합한 HIDL 정의 유형인 fmq_sync 또는 fmq_unsync 유형의 매개변수를 포함한 메서드를 제공합니다. 이 메서드를 사용하여 getDesc()에서 반환된 객체를 수신 프로세스에 전송합니다.

수신 측면:

  1. 설명자 객체를 사용하여 MessageQueue 객체를 생성합니다. 동일한 큐 버전 및 데이터 유형을 사용하지 않으면 템플릿이 컴파일되지 않습니다.
  2. 이벤트 플래그를 추출한 경우 수신 프로세스의 해당 MessageQueue 객체에서 플래그를 추출합니다.
  3. MessageQueue 객체를 사용하여 데이터를 전송합니다.