快速消息隊列 (FMQ)

HIDL 的遠程過程調用 (RPC) 基礎架構使用 Binder 機制,這意味着調用涉及開銷、需要內核操作,並且可以觸發調度程序操作。不過,對於必須在開銷較小且無內核參與的進程之間傳輸數據的情況,則使用快速消息隊列 (FMQ) 系統。

FMQ 會創建具有所需屬性的消息隊列。MQDescriptorSync 或 MQDescriptorUnsync 對象可通過 HIDL RPC 調用發送,並可供接收進程用於訪問消息隊列。

僅 C++ 支持快速消息隊列。

MessageQueue 類型

Android 支持兩種隊列類型(稱爲“風格”):

  • 未同步隊列:可以溢出,並且可以有多個讀取器;每個讀取器都必須及時讀取數據,否則數據將會丟失。
  • 已同步隊列:不能溢出,並且只能有一個讀取器。

這兩種隊列都不能下溢(從空隊列進行讀取將會失敗),並且只能有一個寫入器。

未同步

未同步隊列只有一個寫入器,但可以有任意多個讀取器。此類隊列有一個寫入位置;不過,每個讀取器都會跟蹤各自的獨立讀取位置。

對此類隊列執行寫入操作一定會成功(不會檢查是否出現溢出情況),但前提是寫入的內容不超出配置的隊列容量(如果寫入的內容超出隊列容量,則操作會立即失敗)。由於各個讀取器的讀取位置可能不同,因此每當新的寫入操作需要空間時,系統都允許數據離開隊列,而無需等待每個讀取器讀取每條數據。

讀取操作負責在數據離開隊列末尾之前對其進行檢索。如果讀取操作嘗試讀取的數據超出可用數據量,則該操作要麼立即失敗(如果非阻塞),要麼等到有足夠多的可用數據時(如果阻塞)。如果讀取操作嘗試讀取的數據超出隊列容量,則讀取一定會立即失敗。

如果某個讀取器的讀取速度無法跟上寫入器的寫入速度,則寫入的數據量和該讀取器尚未讀取的數據量加在一起會超出隊列容量,這會導致下一次讀取不會返回數據;相反,該讀取操作會將讀取器的讀取位置重置爲等於最新的寫入位置,然後返回失敗。如果在發生溢出後但在下一次讀取之前,系統查看可供讀取的數據,則會顯示可供讀取的數據超出了隊列容量,這表示發生了溢出。(如果隊列溢出發生在系統查看可用數據和嘗試讀取這些數據之間,則溢出的唯一表徵就是讀取操作失敗。)

已同步

已同步隊列有一個寫入器和一個讀取器,其中寫入器有一個寫入位置,讀取器有一個讀取位置。寫入的數據量不可能超出隊列可提供的空間;讀取的數據量不可能超出隊列當前存在的數據量。如果嘗試寫入的數據量超出可用空間或嘗試讀取的數據量超出現有數據量,則會立即返回失敗,或會阻塞到可以完成所需操作爲止,具體取決於調用的是阻塞還是非阻塞寫入或讀取函數。如果嘗試讀取或嘗試寫入的數據量超出隊列容量,則讀取或寫入操作一定會立即失敗。

設置 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 對象創建消息隊列的第二側。通過 HIDL RPC 調用將 MQDescriptor 對象發送到將容納消息隊列末端的進程。MQDescriptor 包含該隊列的相關信息,其中包括:

  • 用於映射緩衝區和寫入指針的信息。
  • 用於映射讀取指針的信息(如果隊列已同步)。
  • 用於映射事件標記字詞的信息(如果隊列是阻塞隊列)。
  • 對象類型 (<T,flavor>) ,其中包含 HIDL 定義的隊列元素類型和隊列風格(已同步或未同步)。

MQDescriptor 對象可用於構建 MessageQueue 對象:

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

resetPointers 參數表示是否在創建此 MessageQueue 對象時將讀取和寫入位置重置爲 0。在未同步隊列中,讀取位置(在未同步隊列中,是每個 MessageQueue 對象的本地位置)在此對象創建過程中始終設爲 0。通常,MQDescriptor 是在創建第一個消息隊列對象過程中初始化的。要對共享內存進行額外的控制,您可以手動設置 MQDescriptor(MQDescriptor 是在 system/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 */);
  • 長格式:有六個參數(包括事件標記和位掩碼)。支持在多個隊列之間使用共享 EventFlag 對象,並允許指定要使用的通知位掩碼。在這種情況下,必須爲每個讀取和寫入調用提供事件標記和位掩碼。

對於長格式,可在每個 readBlocking() 和 writeBlocking() 調用中顯式提供 EventFlag。可以將其中一個隊列初始化爲包含一個內部事件標記,如果是這樣,則必須使用 getEventFlagWord() 從相應隊列的 MessageQueue 對象中提取該標記,以用於在每個進程中創建與其他 FMQ 一起使用的 EventFlag 對象。或者,可以將 EventFlag 對象初始化爲具有任何合適的共享內存。

一般來說,每個隊列都應只使用以下三項之一:非阻塞、短格式阻塞,或長格式阻塞。混合使用也不算是錯誤;但要獲得理想結果,則需要謹慎地進行編程。

使用 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()。readNotification 和 writeNotification 參數用於指示事件標記中的哪些位應該用於針對相應隊列發出讀取和寫入信號。readNotification 和 writeNotification 是 32 位的位掩碼。

readBlocking() 會等待 writeNotification 位;如果該參數爲 0,則調用一定會失敗。如果 readNotification 值爲 0,則調用不會失敗,但成功的讀取操作將不會設置任何通知位。在已同步隊列中,這意味着相應的 writeBlocking() 調用一定不會喚醒,除非已在其他位置對相應的位進行設置。在未同步隊列中,writeBlocking() 將不會等待(它應仍用於設置寫入通知位),而且對於讀取操作來說,不適合設置任何通知位。同樣,如果 readNotification 爲 0,writeblocking() 將會失敗,並且成功的寫入操作會設置指定的 writeNotification 位。

要一次等待多個隊列,請使用 EventFlag 對象的 wait() 方法來等待通知的位掩碼。wait() 方法會返回一個狀態字詞以及導致系統設置喚醒的位。然後,該信息可用於驗證相應的隊列是否有足夠的控件或數據來完成所需的寫入/讀取操作,並執行非阻塞 write()/read()。要獲取操作後通知,請再次調用 EventFlag 的 wake() 方法。有關 EventFlag 抽象的定義,請參閱 system/libfmq/include/fmq/EventFlag.h。

零複製操作

read/write/readBlocking/writeBlocking() API 會將指向輸入/輸出緩衝區的指針作爲參數,並在內部使用 memcpy() 調用,以便在相應緩衝區和 FMQ 環形緩衝區之間複製數據。爲了提高性能,Android 8.0 及更高版本包含一組 API,這些 API 可提供對環形緩衝區的直接指針訪問,這樣便無需使用 memcpy 調用。

使用以下公共 API 執行零複製 FMQ 操作:

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 結構體(first 和 second),因爲對環形緩衝區執行讀取或寫入操作時可能需要繞回到隊列開頭。這意味着,要對 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 對象表示要讀取/寫入 N 個類型爲 T 的項目的內存區域,則 idx 的有效範圍在 0 到 N-1 之間。
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    將 nMessages 個類型爲 T 的項目寫入到該對象描述的內存區域,從索引 startIdx 開始。此方法使用 memcpy(),但並非旨在用於零複製操作。如果 MemTransaction 對象表示要讀取/寫入 N 個類型爲 T 的項目的內存區域,則 idx 的有效範圍在 0 到 N-1 之間。
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    一種輔助方法,用於從該對象描述的內存區域讀取 nMessages 個類型爲 T 的項目,從索引 startIdx 開始。此方法使用 memcpy(),但並非旨在用於零複製操作。

通過 HIDL 發送隊列

在創建側執行的操作:

  1. 創建消息隊列對象,如上所述。
  2. 使用 isValid() 驗證對象是否有效。
  3. 如果您要通過將 EventFlag 傳遞到長格式的 readBlocking()/writeBlocking() 來等待多個隊列,則可以從經過初始化的 MessageQueue 對象提取事件標記指針(使用 getEventFlagWord())以創建標記,然後使用該標記創建必需的 EventFlag 對象。
  4. 使用 MessageQueue getDesc() 方法獲取描述符對象。
  5. 在 .hal 文件中,爲某個方法提供一個類型爲 fmq_sync 或 fmq_unsync 的參數,其中 T 是 HIDL 定義的一種合適類型。使用此方法將 getDesc() 返回的對象發送到接收進程。

在接收側執行的操作:

  1. 使用描述符對象創建 MessageQueue 對象。務必使用相同的隊列風格和數據類型,否則將無法編譯模板。
  2. 如果您已提取事件標記,則在接收進程中從相應的 MessageQueue 對象提取該標記。
  3. 使用 MessageQueue 對象傳輸數據。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章