FMQ(빠른 메시지 대기열)

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

HIDL의 RPC(원격 프로시저 호출) 인프라는 바인더 메커니즘을 사용합니다. 즉, 호출에는 오버헤드가 수반되고 커널 작업이 필요하며 스케줄러 작업을 트리거할 수 있습니다. 그러나 오버헤드가 적고 커널이 관여하지 않는 프로세스 간에 데이터를 전송해야 하는 경우 FMQ(Fast Message Queue) 시스템이 사용됩니다.

FMQ는 원하는 속성으로 메시지 대기열을 생성합니다. MQDescriptorSync 또는 MQDescriptorUnsync 객체는 HIDL RPC 호출을 통해 전송될 수 있으며 수신 프로세스에서 메시지 큐에 액세스하는 데 사용됩니다.

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

MessageQueue 유형

Android는 두 가지 대기열 유형( flavor 라고 함)을 지원합니다.

  • 동기화 되지 않은 대기열은 오버플로가 허용되며 많은 판독기를 가질 수 있습니다. 각 판독기는 제 시간에 데이터를 읽어야 하며 그렇지 않으면 데이터를 잃어버려야 합니다.
  • 동기화된 대기열은 오버플로가 허용되지 않으며 하나의 판독기만 가질 수 있습니다.

두 대기열 유형 모두 언더플로가 허용되지 않으며(빈 대기열에서 읽기가 실패함) 하나의 작성자만 가질 수 있습니다.

동기화되지 않음

동기화되지 않은 대기열에는 하나의 작성자만 있지만 많은 수의 독자가 있을 수 있습니다. 대기열에는 하나의 쓰기 위치가 있습니다. 그러나 각 리더는 자신의 독립적인 읽기 위치를 추적합니다.

대기열에 대한 쓰기는 구성된 대기열 용량보다 크지 않은 한 항상 성공합니다(오버플로우에 대해 확인되지 않음)(대기열 용량보다 큰 쓰기는 즉시 실패함). 각 판독기는 다른 읽기 위치를 가질 수 있으므로 모든 판독기가 모든 데이터를 읽을 때까지 기다리지 않고 새로운 쓰기에 공간이 필요할 때마다 데이터가 대기열에서 떨어질 수 있습니다.

판독기는 데이터가 대기열 끝에서 떨어지기 전에 데이터를 검색할 책임이 있습니다. 사용 가능한 것보다 더 많은 데이터를 읽으려고 시도하는 읽기는 즉시 실패하거나(비블로킹인 경우) 충분한 데이터가 사용 가능할 때까지 기다립니다(블로킹인 경우). 대기열 용량보다 더 많은 데이터를 읽으려는 읽기는 항상 즉시 실패합니다.

판독기가 기록기를 따라잡지 못하여 해당 판독기가 아직 읽지 않은 데이터의 양이 대기열 용량보다 큰 경우 다음 읽기는 데이터를 반환하지 않습니다. 대신 판독기의 읽기 위치를 최신 쓰기 위치와 동일하게 재설정한 다음 실패를 반환합니다. 오버플로 후 다음 읽기 전에 읽을 수 있는 데이터를 확인하면 큐 용량보다 더 많은 읽을 수 있는 데이터가 표시되어 오버플로가 발생했음을 나타냅니다. (사용 가능한 데이터를 확인하고 해당 데이터를 읽으려고 시도하는 사이에 큐가 오버플로되는 경우 오버플로의 유일한 표시는 읽기가 실패한다는 것입니다.)

비동기 대기열의 독자는 대기열의 읽기 및 쓰기 포인터를 재설정하고 싶지 않을 수 있습니다. 따라서 설명자 판독기에서 대기열을 생성할 때 `resetPointers` 매개변수에 대해 `false` 인수를 사용해야 합니다.

동기화됨

동기화된 대기열에는 단일 쓰기 위치와 단일 읽기 위치가 있는 하나의 작성자와 하나의 판독기가 있습니다. 큐에 있는 공간보다 더 많은 데이터를 쓰거나 큐가 현재 보유하고 있는 것보다 더 많은 데이터를 읽는 것은 불가능합니다. 차단 또는 비차단 쓰기 또는 읽기 기능이 호출되었는지 여부에 따라 사용 가능한 공간 또는 데이터를 초과하려는 시도는 즉시 실패를 반환하거나 원하는 작업이 완료될 수 있을 때까지 차단됩니다. 대기열 용량보다 더 많은 데이터를 읽거나 쓰려는 시도는 항상 즉시 실패합니다.

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 는 동기화된 대기열의 경우 kUnsynchronizedWrite 이거나 비동기 대기열의 경우 kSynchronizedReadWrite 일 수 있습니다.
  • 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 객체를 생성할 수 있습니다.

대기열 및 이벤트 플래그 차단

기본적으로 큐는 차단 읽기/쓰기를 지원하지 않습니다. 차단 읽기/쓰기 호출에는 두 가지 종류가 있습니다.

  • 세 개의 매개변수(데이터 포인터, 항목 수, 시간 초과)가 있는 짧은 형식 . 단일 대기열에서 개별 읽기/쓰기 작업에 대한 차단을 지원합니다. 이 형식을 사용할 때 큐는 이벤트 플래그와 비트마스크를 내부적으로 처리하며 첫 번째 메시지 큐 객체 는 두 번째 매개변수 true 로 초기화해야 합니다. 예:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Long form, with six parameters (includes event flag and bitmasks). Supports using a shared EventFlag object between multiple queues and allows specifying the notification bit masks to be used. In this case, the event flag and bitmasks must be supplied to each read and write call.

For the long form, the EventFlag can be supplied explicitly in each readBlocking() and writeBlocking() call. One of the queues may be initialized with an internal event flag, which must then be extracted from that queue's MessageQueue objects using getEventFlagWord() and used to create EventFlag objects in each process for use with other FMQs. Alternatively, the EventFlag objects can be initialized with any suitable shared memory.

In general, each queue should use only one of non-blocking, short-form blocking, or long-form blocking. It is not an error to mix them, but careful programming is required to get the desired result.

Using the MessageQueue

The public API of the MessageQueue object is:

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() 에 대한 자체 계산을 수행합니다.
  • 느린 판독기의 관점에서 볼 때 대기열이 넘칠 수 있습니다. 이로 인해 queue 크기보다 큰 값을 반환하는 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() 은 대기하지 않으며(여전히 쓰기 알림 비트를 설정하는 데 사용해야 함) 읽기가 알림 비트를 설정하지 않는 것이 적절합니다. 마찬가지로 readNotification 이 0이면 writeblocking() 이 실패하고 성공적인 쓰기는 지정된 writeNotification 비트를 설정합니다.

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

제로 복사 작업

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 구조체에는 두 개의 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. 긴 형식의 readBlocking() / writeBlocking()EventFlag 를 전달하여 여러 대기열에서 대기하는 경우 플래그를 만들기 위해 초기화된 MessageQueue 개체에서 이벤트 플래그 포인터( getEventFlagWord() 사용)를 추출할 수 있습니다. 해당 플래그를 사용하여 필요한 EventFlag 개체를 만듭니다.
  4. MessageQueue getDesc() 메서드를 사용하여 설명자 개체를 가져옵니다.
  5. .hal 파일에서 메소드에 fmq_sync 유형의 매개변수를 지정하십시오. 또는 fmq_unsync 여기서 T 는 적합한 HIDL 정의 유형입니다. 이를 사용하여 getDesc() 에서 반환된 개체를 수신 프로세스로 보냅니다.

받는 쪽에서:

  1. 설명자 개체를 사용하여 MessageQueue 개체를 만듭니다. 동일한 대기열 플레이버와 데이터 유형을 사용해야 합니다. 그렇지 않으면 템플릿이 컴파일되지 않습니다.
  2. 이벤트 플래그를 추출한 경우 수신 프로세스의 해당 MessageQueue 개체에서 플래그를 추출합니다.
  3. MessageQueue 개체를 사용하여 데이터를 전송합니다.