Android HIDL學習(6)---Fast Message Queue

想聊聊FMQ的,無意中看到下面這篇文章,寫的很好,所以就直接拿來用了,笑納笑納~
http://www.lxweimin.com/p/5c6e35c7c346

快速消息隊列 (FMQ)

HIDL 的遠程過程調(diào)用 (RPC) 基礎架構使用 Binder 機制,這意味著調(diào)用涉及開銷、需要內(nèi)核操作,并且可以觸發(fā)調(diào)度程序操作。

不過,對于必須在開銷較小且無內(nèi)核參與的進程之間傳輸數(shù)據(jù)的情況,則使用快速消息隊列 (FMQ) 系統(tǒng)。

FMQ 會創(chuàng)建具有所需屬性的消息隊列。MQDescriptorSync 或 MQDescriptorUnsync 對象可通過 HIDL RPC 調(diào)用發(fā)送,并可供接收進程用于訪問消息隊列。

MessageQueue 類型

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

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

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

未同步

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

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

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

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

已同步

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

設置 FMQ

一個消息隊列需要多個 MessageQueue 對象:一個對象用作數(shù)據(jù)寫入目標位置,以及一個或多個對象用作數(shù)據(jù)讀取來源。沒有關于哪些對象用于寫入數(shù)據(jù)或讀取數(shù)據(jù)的顯式配置;用戶需負責確保沒有對象既用于讀取數(shù)據(jù)又用于寫入數(shù)據(jù),也就是說最多只有一個寫入器,并且對于已同步隊列,最多只有一個讀取器。

創(chuàng)建第一個 MessageQueue 對象

通過單個調(diào)用創(chuàng)建并配置消息隊列:

#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) 初始化程序負責創(chuàng)建并初始化支持消息隊列功能的對象。
  • MessageQueue<T, flavor>(numElements, configureEventFlagWord) 初始化程序負責創(chuàng)建并初始化支持消息隊列功能和阻塞的對象。
  • flavor 可以是 kSynchronizedReadWrite(對于已同步隊列)或 kUnsynchronizedWrite(對于未同步隊列)。
  • uint16_t(在本示例中)可以是任意不涉及嵌套式緩沖區(qū)(無 stringvec 類型)、句柄或接口的 HIDL 定義的類型
  • kNumElementsInQueue 表示隊列的大小(以條目數(shù)表示);它用于確定將為隊列分配的共享內(nèi)存緩沖區(qū)的大小。

創(chuàng)建第二個 MessageQueue 對象

使用從消息隊列的第一側獲取的 MQDescriptor 對象創(chuàng)建消息隊列的第二側。通過 HIDL RPC 調(diào)用將 MQDescriptor 對象發(fā)送到將容納消息隊列末端的進程。MQDescriptor 包含該隊列的相關信息,其中包括:

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

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

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

resetPointers 參數(shù)表示是否在創(chuàng)建此 MessageQueue 對象時將讀取和寫入位置重置為 0。在未同步隊列中,讀取位置(在未同步隊列中,是每個 MessageQueue 對象的本地位置)在此對象創(chuàng)建過程中始終設為 0。通常,MQDescriptor 是在創(chuàng)建第一個消息隊列對象過程中初始化的。要對共享內(nèi)存進行額外的控制,您可以手動設置 MQDescriptorMQDescriptor 是在 system/libhidl/base/include/hidl/MQDescriptor.h中定義的),然后按照本部分所述內(nèi)容創(chuàng)建每個 MessageQueue 對象。

阻塞隊列和事件標記

默認情況下,隊列不支持阻塞讀取/寫入。有兩種類型的阻塞讀取/寫入調(diào)用:

  • 短格式:有三個參數(shù)(數(shù)據(jù)指針、項數(shù)、超時)。支持阻塞針對單個隊列的各個讀取/寫入操作。在使用這種格式時,隊列將在內(nèi)部處理事件標記和位掩碼,并且第一個消息隊列對象必須初始化為第二個參數(shù)為 true。例如:
// For an unsynchronized FMQ that supports blocking
mFmqUnsynchronizedBlocking =
  new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
      (kNumElementsInQueue, true /* enable blocking operations */);

  • 長格式:有六個參數(shù)(包括事件標記和位掩碼)。支持在多個隊列之間使用共享 EventFlag 對象,并允許指定要使用的通知位掩碼。在這種情況下,必須為每個讀取和寫入調(diào)用提供事件標記和位掩碼。

對于長格式,可在每個 readBlocking() 和 writeBlocking() 調(diào)用中顯式提供 EventFlag。

可以將其中一個隊列初始化為包含一個內(nèi)部事件標記,如果是這樣,則必須使用 getEventFlagWord() 從相應隊列的 MessageQueue 對象中提取該標記,以用于在每個進程中創(chuàng)建與其他 FMQ 一起使用的 EventFlag 對象。或者,可以將 EventFlag 對象初始化為具有任何合適的共享內(nèi)存。

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

使用 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);

1. availableToWrite() 和 availableToRead() 可用于確定在一次操作中可傳輸?shù)臄?shù)據(jù)量。
在未同步隊列中:

  • availableToWrite() 始終返回隊列容量。
  • 每個讀取器都有自己的讀取位置,并會針對 availableToRead() 進行自己的計算。
  • 如果是讀取速度緩慢的讀取器,隊列可以溢出,這可能會導致 availableToRead() 返回的值大于隊列的大小。發(fā)生溢出后進行的第一次讀取操作將會失敗,并且會導致相應讀取器的讀取位置被設為等于當前寫入指針,無論是否通過 availableToRead() 報告了溢出都是如此。

2. 如果所有請求的數(shù)據(jù)都可以(并已)傳輸?shù)疥犃?從隊列傳出,則 read() 和 write() 方法會返回 true。這些方法不會阻塞;它們要么成功(并返回 true),要么立即返回失敗 (false)。

3. readBlocking() 和 writeBlocking() 方法會等到可以完成請求的操作,或等到超時(timeOutNanos 值為 0 表示永不超時)。
阻塞操作使用事件標記字詞來實現(xiàn)。
默認情況下,每個隊列都會創(chuàng)建并使用自己的標記字詞來支持短格式的 readBlocking() 和 writeBlocking()。
多個隊列可以共用一個字詞,這樣一來,進程就可以等待對任何隊列執(zhí)行寫入或讀取操作。可
以通過調(diào)用 getEventFlagWord() 獲得指向隊列事件標記字詞的指針,此類指針(或任何指向合適的共享內(nèi)存位置的指針)可用于創(chuàng)建 EventFlag 對象,以傳遞到其他隊列的長格式 readBlocking() 和 writeBlocking()。readNotification 和 writeNotification 參數(shù)用于指示事件標記中的哪些位應該用于針對相應隊列發(fā)出讀取和寫入信號。readNotification 和 writeNotification 是 32 位的位掩碼。
readBlocking() 會等待 writeNotification 位;如果該參數(shù)為 0,則調(diào)用一定會失敗。
如果 readNotification 值為 0,則調(diào)用不會失敗,但成功的讀取操作將不會設置任何通知位。
在已同步隊列中,這意味著相應的 writeBlocking() 調(diào)用一定不會喚醒,除非已在其他位置對相應的位進行設置
。在未同步隊列中,writeBlocking() 將不會等待(它應仍用于設置寫入通知位),而且對于讀取操作來說,不適合設置任何通知位。
同樣,如果 readNotification 為 0,writeblocking() 將會失敗,并且成功的寫入操作會設置指定的 writeNotification 位。
要一次等待多個隊列,請使用 EventFlag 對象的 wait() 方法來等待通知的位掩碼。wait() 方法會返回一個狀態(tài)字詞以及導致系統(tǒng)設置喚醒的位。然后,該信息可用于驗證相應的隊列是否有足夠的控件或數(shù)據(jù)來完成所需的寫入/讀取操作,并執(zhí)行非阻塞 write()/read()。要獲取操作后通知,請再次調(diào)用 EventFlag 的 wake() 方法。

零復制操作

read/write/readBlocking/writeBlocking() API 會將指向輸入/輸出緩沖區(qū)的指針作為參數(shù),并在內(nèi)部使用 memcpy() 調(diào)用,以便在相應緩沖區(qū)和 FMQ 環(huán)形緩沖區(qū)之間復制數(shù)據(jù)。為了提高性能,Android 8.0 及更高版本包含一組 API,這些 API 可提供對環(huán)形緩沖區(qū)的直接指針訪問,這樣便無需使用 memcpy 調(diào)用。

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 環(huán)形緩沖區(qū)的基址指針。在數(shù)據(jù)寫入之后,使用 commitWrite() 提交數(shù)據(jù)。beginRead/commitRead 方法的運作方式與之相同。
  • beginRead/Write 方法會將要讀取/寫入的消息條數(shù)視為輸入,并會返回一個布爾值來指示是否可以執(zhí)行讀取/寫入操作。如果可以執(zhí)行讀取或寫入操作,則 memTx 結構體中會填入基址指針,這些指針可用于對環(huán)形緩沖區(qū)共享內(nèi)存進行直接指針訪問。
  • MemRegion 結構體包含有關內(nèi)存塊的詳細信息,其中包括基礎指針(內(nèi)存塊的基址)和以 T 表示的長度(以 HIDL 定義的消息隊列類型表示的內(nèi)存塊長度)。
  • MemTransaction 結構體包含兩個 MemRegion 結構體(first 和 second),因為對環(huán)形緩沖區(qū)執(zhí)行讀取或寫入操作時可能需要繞回到隊列開頭。這意味著,要對 FMQ 環(huán)形緩沖區(qū)執(zhí)行數(shù)據(jù)讀取/寫入操作,需要兩個基址指針。

從 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 對象內(nèi)的第一個和第二個 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 內(nèi)的槽位 idx。如果 MemTransaction 對象表示要讀取/寫入 N 個類型為 T 的項目的內(nèi)存區(qū)域,則 idx 的有效范圍在 0 到 N-1 之間。

  • bool copyTo(const T * data, size_t startIdx, size_t nMessages = 1);
    將 nMessages 個類型為 T 的項目寫入到該對象描述的內(nèi)存區(qū)域,從索引 startIdx 開始。此方法使用 memcpy(),但并非旨在用于零復制操作。如果 MemTransaction 對象表示要讀取/寫入 N 個類型為 T 的項目的內(nèi)存區(qū)域,則 idx 的有效范圍在 0 到 N-1 之間。

  • bool copyFrom(T * data, size_t startIdx, size_t nMessages = 1);
    一種輔助方法,用于從該對象描述的內(nèi)存區(qū)域讀取 nMessages 個類型為 T 的項目,從索引 startIdx 開始。此方法使用 memcpy(),但并非旨在用于零復制操作。

HIDL消息隊列的使用方法總結

在創(chuàng)建側執(zhí)行的操作:

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

在接收側執(zhí)行的操作:

1. 使用描述符對象創(chuàng)建 MessageQueue 對象。務必使用相同的隊列風格和數(shù)據(jù)類型,否則將無法編譯模板。
2. 如果您已提取事件標記,則在接收進程中從相應的 MessageQueue 對象提取該標記。
3. 使用 MessageQueue 對象傳輸數(shù)據(jù)。

使用 Binder IPC

從 Android O 開始,Android 框架和 HAL 現(xiàn)在使用 Binder 互相通信。由于這種通信方式極大地增加了 Binder 流量,因此 Android O 包含了幾項改進,旨在確保 Binder IPC 的速度。集成最新版 Binder 驅動程序的 SoC 供應商和原始設備制造商 (OEM) 應該查看這些改進的列表、用于 3.18、4.4 和 4.9 版內(nèi)核的相關 SHA,以及所需的用戶空間更改。

多個 Binder 域(上下文)

為了明確地拆分框架(與設備無關)和供應商(與具體設備相關)代碼之間的 Binder 流量,Android O 引入了“Binder 上下文”這一概念。每個 Binder 上下文都有自己的設備節(jié)點和上下文(服務)管理器。您只能通過上下文管理器所屬的設備節(jié)點對其進行訪問,并且在通過特定上下文傳遞 Binder 節(jié)點時,只能由另一個進程從相同的上下文訪問上下文管理器,從而確保這些域完全互相隔離。如需使用方法的詳細信息,請參閱 vndbindervndservicemanager

分散-集中

在之前的 Android 版本中,Binder 調(diào)用中的每條數(shù)據(jù)都會被復制 3 次:

  • 一次是在調(diào)用進程中將數(shù)據(jù)序列化為 Parce
  • 一次是在內(nèi)核驅動程序中將 Parcel 復制到目標進程
  • 一次是在目標進程中對 Parcel 進行反序列化

Android O 使用分散-集中優(yōu)化機制將復制次數(shù)從 3 次減少到了 1 次。數(shù)據(jù)保留其原始結構和內(nèi)存布局,且 Binder 驅動程序會立即將數(shù)據(jù)復制到目標進程中,而不是先在 Parcel 中對數(shù)據(jù)進行序列化。在目標進程中,這些數(shù)據(jù)的結構和內(nèi)存布局保持不變,并且,在無需再次復制的情況下即可讀取這些數(shù)據(jù)。

vndbinder

Android O 支持供應商服務使用新的 Binder 域,這可通過使用 /dev/vndbinder(而非 /dev/binder)進行訪問。添加 /dev/vndbinder 后,Android 現(xiàn)在擁有以下 3 個 IPC 域:

IPC 域 說明
/dev/binder 框架/應用進程之間的 IPC,使用 AIDL 接口
dev/hwbinder 框架/供應商進程之間的 IPC,使用 HIDL 接口供應商進程之間的 IPC,使用 HIDL 接口
/dev/vndbinder 供應商/供應商進程之間的 IPC,使用 AIDL 接口

HIDL Memory Block

HIDL 內(nèi)存塊是一個建立在HIDL @1.0::IAllocator, 和 HIDL @1.0::IMapper的抽象層。
它是為具有多個內(nèi)存塊共享單個內(nèi)存堆的HIDL Severis而設計的。

結構

HIDL內(nèi)存塊體系結構包括多個內(nèi)存塊共享一個內(nèi)存堆的HIDL services:

image

使用實例

聲明HAL

IFoo HAL:

import android.hidl.memory.block@1.0::MemoryBlock;

interface IFoo {
    getSome() generates(MemoryBlock block);
    giveBack(MemoryBlock block);
};

Android.bp:

hidl_interface {
    ...
    srcs: [
        "IFoo.hal",
    ],
    interfaces: [
        "android.hidl.memory.block@1.0",
        ...
};

實施HAL

1.獲取 hidl_memory

#include <android/hidl/allocator/1.0/IAllocator.h>

using ::android::hidl::allocator::V1_0::IAllocator;
using ::android::hardware::hidl_memory;
...
  sp<IAllocator> allocator = IAllocator::getService("ashmem");
  allocator->allocate(2048, [&](bool success, const hidl_memory& mem)
  {
        if (!success) { /* error */ }
        // you can now use the hidl_memory object 'mem' or pass it
  }));

2.創(chuàng)建一個HidlMemoryDealer來獲取hidl_memory:

#include <hidlmemory/HidlMemoryDealer.h>

using ::android::hardware::HidlMemoryDealer
/* The mem argument is acquired in the Step1, returned by the ashmemAllocator->allocate */
sp<HidlMemoryDealer> memory_dealer = HidlMemoryDealer::getInstance(mem);

3.使用MemoryBlock申請內(nèi)存

struct MemoryBlock {
IMemoryToken token;
uint64_t size;
uint64_t offset;
};

#include <android/hidl/memory/block/1.0/types.h>

using ::android::hidl::memory::block::V1_0::MemoryBlock;

Return<void> Foo::getSome(getSome_cb _hidl_cb) {
    MemoryBlock block = memory_dealer->allocate(1024);
    if(HidlMemoryDealer::isOk(block)){
        _hidl_cb(block);
    ...

  1. 解除分配:
Return<void> Foo::giveBack(const MemoryBlock& block) {
    memory_dealer->deallocate(block.offset);
...

5.使用數(shù)據(jù)

#include <hidlmemory/mapping.h>
#include <android/hidl/memory/1.0/IMemory.h>

using ::android::hidl::memory::V1_0::IMemory;

sp<IMemory> memory = mapMemory(block);
uint8_t* data =

static_cast<uint8_t*>(static_cast<void*>(memory->getPointer()));

6.配置 Android.bp

shared_libs: [
        "android.hidl.memory@1.0",

        "android.hidl.memory.block@1.0"

        "android.hidl.memory.token@1.0",
        "libhidlbase",
        "libhidlmemory",

線程模型

注意:
標記為 oneway 的方法不會阻塞。對于未標記為 oneway 的方法,在服務器完成執(zhí)行任務或調(diào)用同步回調(diào)(以先發(fā)生者為準)之前,客戶端的方法調(diào)用將一直處于阻塞狀態(tài)。
服務器方法實現(xiàn)最多可以調(diào)用一個同步回調(diào);多出的回調(diào)調(diào)用會被舍棄并記錄為錯誤。如果方法應通過回調(diào)返回值,但未調(diào)用其回調(diào),系統(tǒng)會將這種情況記錄為錯誤,并作為傳輸錯誤報告給客戶端。

直通模式下的線程

在直通模式下,大多數(shù)調(diào)用都是同步的。不過,為確保 oneway 調(diào)用不會阻塞客戶端這一預期行為,系統(tǒng)會分別為每個進程創(chuàng)建線程。

綁定式 HAL 中的線程

為了處理傳入的 RPC 調(diào)用(包括從 HAL 到 HAL 用戶的異步回調(diào))和終止通知,系統(tǒng)會為使用 HIDL 的每個進程關聯(lián)一個線程池。
如果單個進程實現(xiàn)了多個 HIDL 接口和/或終止通知處理程序,則所有這些接口和/或處理程序會共享其線程池。當進程接收從客戶端傳入的方法調(diào)用時,它會從線程池中選擇一個空閑線程,并在該線程上執(zhí)行調(diào)用。如果沒有空閑的線程,它將會阻塞,直到有可用線程為止。

如果服務器只有一個線程,則傳入服務器的調(diào)用將按順序完成。具有多個線程的服務器可以不按順序完成調(diào)用,即使客戶端只有一個線程也是如此

不過,對于特定的接口對象,oneway 調(diào)用會保證按順序進行(請參閱服務器線程模型。對于托管了多個界面的多線程服務器,對不同界面的多項 oneway 調(diào)用可能會并行處理,也可能會與其他阻塞調(diào)用并行處理。

服務器線程模型

(直通模式除外)HIDL 接口的服務器實現(xiàn)位于不同于客戶端的進程中,并且需要一個或多個線程等待傳入的方法調(diào)用。

這些線程構成服務器的線程池;服務器可以決定它希望在其線程池中運行多少線程,并且可以利用一個線程大小的線程池來按順序處理其接口上的所有調(diào)用。如果服務器的線程池中有多個線程,則服務器可以在其任何接口上接收同時傳入的調(diào)用(在 C++ 中,這意味著必須小心鎖定共享數(shù)據(jù))。

傳入同一接口的單向調(diào)用會按順序進行處理。如果多線程客戶端在接口 IFoo 上調(diào)用 method1 和 method2,并在接口 IBar 上調(diào)用 method3,則 method1 和 method2 將始終按順序運行,但 method3 可以與 method1 和 method2 并行運行。

單一客戶端執(zhí)行線程可能會通過以下兩種方式在具有多個線程的服務器上引發(fā)并行運行:

  • oneway 調(diào)用不會阻塞。如果執(zhí)行 oneway 調(diào)用,然后調(diào)用非 oneway,則服務器可以同時執(zhí)行 oneway 調(diào)用和非 oneway 調(diào)用。
  • 當系統(tǒng)從服務器調(diào)用回調(diào)時,通過同步回調(diào)傳回數(shù)據(jù)的服務器方法可以立即解除對客戶端的阻塞。

客戶端線程模型

非阻塞調(diào)用(帶有 oneway 關鍵字標記的函數(shù))與阻塞調(diào)用(未指定 oneway 關鍵字的函數(shù))的客戶端線程模型有所不同。

阻塞調(diào)用

對于阻塞調(diào)用來說,除非發(fā)生以下情況之一,否則客戶端將一直處于阻塞狀態(tài):

  • 出現(xiàn)傳輸錯誤;Return 對象包含可通過 Return::isOk() 檢索的錯誤狀態(tài)。
  • 服務器實現(xiàn)調(diào)用回調(diào)(如果有)。
  • 服務器實現(xiàn)返回值(如果沒有回調(diào)參數(shù))。

如果成功的話,客戶端以參數(shù)形式傳遞的回調(diào)函數(shù)始終會在函數(shù)本身返回之前被服務器調(diào)用。回調(diào)是在進行函數(shù)調(diào)用的同一線程上執(zhí)行,所以在函數(shù)調(diào)用期間,實現(xiàn)人員必須謹慎地持有鎖(并盡可能徹底避免持有鎖)。不含 generates 語句或 oneway 關鍵字的函數(shù)仍處于阻塞狀態(tài);在服務器返回 Return<void> 對象之前,客戶端將一直處于阻塞狀態(tài)。

單向調(diào)用

如果某個函數(shù)標記有 oneway,則客戶端會立即返回,而不會等待服務器完成其函數(shù)調(diào)用。

數(shù)據(jù)類型

本節(jié)只列舉C++的相關數(shù)據(jù)類型。

HIDL 類型 C++ 類型 頭文件/庫
enum enum class
uint8_t..uint64_t uint8_t..uint64_t <stdint.h>
int8_t..int64_t int8_t..int64_t <stdint.h>
float float
double double
vec<T> hidl_vec<T> libhidlbase
T[S1][S2]...[SN] T[S1][S2]...[SN]
string hidl_string libhidlbase
handle hidl_handle libhidlbase
opaque uint64_t <stdint.h>
struct struct
union union
fmq_sync MQDescriptorSync libhidlbase
fmq_unsync MQDescriptorUnsync libhidlbase

枚舉

HIDL 形式的枚舉會變成 C++ 形式的枚舉。例如:

enum Mode : uint8_t { WRITE = 1 << 0, READ = 1 << 1 };

變?yōu)椋?/p>

enum class Mode : uint8_t { WRITE = 1, READ = 2 };

bitfield<T>

bitfield<T>(其中 T 是用戶定義的枚舉)會變?yōu)?C++ 形式的該枚舉的底層類型。在上述示例中,bitfield<Mode> 會變?yōu)?uint8_t。

vec<T>

hidl_vec<T> 類模板是 libhidlbase 的一部分,可用于傳遞具備任意大小的任何 HIDL 類型的矢量。與之相當?shù)木哂泄潭ù笮〉娜萜魇?hidl_array。此外,您也可以使用 hidl_vec::setToExternal() 函數(shù)將 hidl_vec<T> 初始化為指向 T 類型的外部數(shù)據(jù)緩沖區(qū)。

除了在生成的 C++ 頭文件中適當?shù)匕l(fā)出/插入結構之外,您還可以使用 vec<T> 生成一些便利函數(shù),用于轉換到 std::vector 和 T 裸指針或從它們進行轉換。如果您將 vec<T> 用作參數(shù),則使用它的函數(shù)將過載(將生成兩個原型),以接受并傳遞該參數(shù)的 HIDL 結構和 std::vector<T> 類型。

數(shù)組

hidl 中的常量數(shù)組由 libhidlbase 中的 hidl_array 類表示。hidl_array<T, S1, S2, …, SN> 表示具有固定大小的 N 維數(shù)組 T[S1][S2]…[SN]。

字符串

hidl_string 類(libhidlbase 的一部分)可用于通過 HIDL 接口傳遞字符串,并在 /system/libhidl/base/include/hidl/HidlSupport.h 下進行定義。該類中的第一個存儲位置是指向其字符緩沖區(qū)的指針。

struct

HIDL 形式的 struct 只能包含固定大小的數(shù)據(jù)類型,不能包含任何函數(shù)。HIDL 結構定義會直接映射到 C++ 形式的標準布局 struct,從而確保 struct 具有一致的內(nèi)存布局。一個struct可以包括多種指向單獨的可變長度緩沖區(qū)的 HIDL 類型(包括 handle、string 和 vec<T>)。

handle

handle 類型由 C++ 形式的 hidl_handle 結構表示,該結構是一個簡單的封裝容器,用于封裝指向 const native_handle_t 對象的指針(該對象已經(jīng)在 Android 中存在了很長時間)。

typedef struct native_handle
{
    int version;        /* sizeof(native_handle_t) */
    int numFds;         /* number of file descriptors at &data[0] */
    int numInts;        /* number of ints at &data[numFds] */
    int data[0];        /* numFds + numInts ints */
} native_handle_t;

memory

HIDL memory 類型會映射到 libhidlbase 中的 hidl_memory 類,該類表示未映射的共享內(nèi)存。這是要在 HIDL 中共享內(nèi)存而必須在進程之間傳遞的對象。要使用共享內(nèi)存,需滿足以下條件:
1.獲取 IAllocator 的實例(當前只有“ashmem”實例可用)并使用該實例分配共享內(nèi)存。
2.IAllocator::allocate() 返回 hidl_memory 對象,該對象可通過 HIDL RPC 傳遞,并能使用 libhidlmemory 的 mapMemory 函數(shù)映射到某個進程。
3.mapMemory 返回對可用于訪問內(nèi)存的 sp<IMemory> 對象的引用(IMemory 和 IAllocator 在 android.hidl.memory@1.0 中定義)。

IAllocator 的實例可用于分配內(nèi)存:

#include <android/hidl/allocator/1.0/IAllocator.h>
#include <android/hidl/memory/1.0/IMemory.h>
#include <hidlmemory/mapping.h>
using ::android::hidl::allocator::V1_0::IAllocator;
using ::android::hidl::memory::V1_0::IMemory;
using ::android::hardware::hidl_memory;
....
  sp<IAllocator> ashmemAllocator = IAllocator::getService("ashmem");
  ashmemAllocator->allocate(2048, [&](bool success, const hidl_memory& mem) {
        if (!success) { /* error */ }
        // now you can use the hidl_memory object 'mem' or pass it around
  }));

對內(nèi)存的實際更改必須通過 IMemory 對象完成(在創(chuàng)建 mem 的一端或在通過 HIDL RPC 接收更改的一端完成):

// Same includes as above

sp<IMemory> memory = mapMemory(mem);
void* data = memory->getPointer();
memory->update();
// update memory however you wish after calling update and before calling commit
data[0] = 42;
memory->commit();
// …
memory->update(); // the same memory can be updated multiple times
// …
memory->commit();

接口

接口可作為對象傳遞。“接口”一詞可用作 android.hidl.base@1.0::IBase 類型的語法糖;此外,當前的接口以及任何導入的接口都將定義為一個類型。

存儲接口的變量應該是強指針:sp<IName>。接受接口參數(shù)的 HIDL 函數(shù)會將原始指針轉換為強指針,從而導致不可預料的行為(可能會意外清除指針)。為避免出現(xiàn)問題,請務必將 HIDL 接口存儲為 sp<>。

創(chuàng)建 HAL 客戶端

首先將 HAL 庫添加到 makefile 中:

Make:LOCAL_SHARED_LIBRARIES += android.hardware.nfc@1.0
Soong:shared_libs: [ …, android.hardware.nfc@1.0 ]

接下來,添加 HAL 頭文件:

#include <android/hardware/nfc/1.0/IFoo.h>
…
// in code:
sp<IFoo> client = IFoo::getService();
client->doThing();

創(chuàng)建 HAL 服務器

要創(chuàng)建 HAL 實現(xiàn),您必須具有表示 HAL 的 .hal 文件并已在 hidl-gen 上使用 -Lmakefile 或 -Landroidbp 為 HAL 生成 makefile(./hardware/interfaces/update-makefiles.sh 會為內(nèi)部 HAL 文件執(zhí)行這項操作,這是一個很好的參考)。從 libhardware 通過 HAL 傳輸時,您可以使用 c2hal 輕松完成許多此類工作。

要創(chuàng)建必要的文件來實現(xiàn)您的 HAL,請使用以下代碼:

PACKAGE=android.hardware.nfc@1.0
LOC=hardware/interfaces/nfc/1.0/default/
m -j hidl-gen
hidl-gen -o $LOC -Lc++-impl -randroid.hardware:hardware/interfaces \
    -randroid.hidl:system/libhidl/transport $PACKAGE
hidl-gen -o $LOC -Landroidbp-impl -randroid.hardware:hardware/interfaces \
    -randroid.hidl:system/libhidl/transport $PACKAGE

接下來,使用相應功能填寫存根并設置守護進程。守護進程代碼(支持直通)示例:

#include <hidl/LegacySupport.h>

int main(int /* argc */, char* /* argv */ []) {
    return defaultPassthroughServiceImplementation<INfc>("nfc");
}

defaultPassthroughServiceImplementation 將對提供的 -impl 庫執(zhí)行 dlopen() 操作,并將其作為綁定式服務提供。守護進程代碼(對于純綁定式服務)示例:

int main(int /* argc */, char* /* argv */ []) {
    // This function must be called before you join to ensure the proper
    // number of threads are created. The threadpool will never exceed
    // size one because of this call.
    ::android::hardware::configureRpcThreadpool(1 /*threads*/, true /*willJoin*/);

    sp nfc = new Nfc();
    const status_t status = nfc->registerAsService();
    if (status != ::android::OK) {
        return 1; // or handle error
    }

    // Adds this thread to the threadpool, resulting in one total
    // thread in the threadpool. We could also do other things, but
    // would have to specify 'false' to willJoin in configureRpcThreadpool.
    ::android::hardware::joinRpcThreadpool();
    return 1; // joinRpcThreadpool should never return
}

此守護進程通常存在于 $PACKAGE + "-service-suffix"(例如 android.hardware.nfc@1.0-service)中,但也可以位于任何位置。HAL 的特定類的 sepolicy 是屬性 hal_<module>(例如 hal_nfc))。您必須將此屬性應用到運行特定 HAL 的守護進程(如果同一進程提供多個 HAL,則可以將多個屬性應用到該進程)。

軟件包

HIDL 接口軟件包位于 hardware/interfaces 或 vendor/ 目錄下(少數(shù)例外情況除外)。hardware/interfaces 頂層會直接映射到 android.hardware 軟件包命名空間;版本是軟件包(而不是接口)命名空間下的子目錄。

hidl-gen 編譯器會將 .hal 文件編譯成一組 .h 和 .cpp 文件。這些自動生成的文件可用來編譯客戶端/服務器實現(xiàn)鏈接到的共享庫。用于編譯此共享庫的 Android.bp 文件由 hardware/interfaces/update-makefiles.sh 腳本自動生成。每次將新軟件包添加到 hardware/interfaces 或在現(xiàn)有軟件包中添加/移除 .hal 文件時,您都必須重新運行該腳本,以確保生成的共享庫是最新的。

例如,IFoo.hal 示例文件應該位于 hardware/interfaces/samples/1.0 下。IFoo.hal 示例文件可以在 samples 軟件包中創(chuàng)建一個 IFoo 接口:

package android.hardware.samples@1.0;
interface IFoo {
    struct Foo {
       int64_t someValue;
       handle  myHandle;
    };

    someMethod() generates (vec<uint32_t>);
    anotherMethod(Foo foo) generates (int32_t ret);
};

生成的文件

HIDL 軟件包中自動生成的文件會鏈接到與軟件包同名的單個共享庫(例如 android.hardware.samples@1.0)。該共享庫還會導出單個標頭 IFoo.h,用于包含在客戶端和服務器中。綁定式模式使用 hidl-gen 編譯器并以 IFoo.hal 接口文件作為輸入,它具有以下自動生成的文件:

image

鏈接到共享庫

使用軟件包中的任何接口的客戶端或服務器必須在下面的其中一 (1) 個位置包含該軟件包的共享庫:
在 Android.mk 中:

LOCAL_SHARED_LIBRARIES += android.hardware.samples@1.0

在 Android.bp 中:

shared_libs: [
    /* ... */
    "android.hardware.samples@1.0",
],

image

命名空間

HIDL 函數(shù)和類型(如 Return<T> 和 Void())已在命名空間 ::android::hardware 中進行聲明。軟件包的 C++ 命名空間由軟件包的名稱和版本號確定。例如,hardware/interfaces 下版本為 1.2 的軟件包 mypackage 具有以下特質(zhì):

  • C++ 命名空間是 ::android::hardware::mypackage::V1_2
  • 該軟件包中 IMyInterface 的完全限定名稱是
    ::android::hardware::mypackage::V1_2::IMyInterface(IMyInterface 是一個標識符,而不是命名空間的一部分)。
  • 在軟件包的 types.hal 文件中定義的類型標識為 ::android::hardware::mypackage::V1_2::MyPackageType

學習算是告一段落,東西太多了,消化消化,接下來開始實戰(zhàn)。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內(nèi)容

  • 快速消息隊列 (FMQ) HIDL 的遠程過程調(diào)用 (RPC) 基礎架構使用 Binder 機制,這意味著調(diào)用涉及...
    Lee_5566閱讀 8,824評論 0 8
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,120評論 1 32
  • ??一個任務通常就是一個程序,每個運行中的程序就是一個進程。當一個程序運行時,內(nèi)部可能包含了多個順序執(zhí)行流,每個順...
    OmaiMoon閱讀 1,692評論 0 12
  • 一、簡歷準備 1、個人技能 (1)自定義控件、UI設計、常用動畫特效 自定義控件 ①為什么要自定義控件? Andr...
    lucas777閱讀 5,228評論 2 54
  • Android消息處理機制估計都被寫爛了,但是依然還是要寫一下,因為Android應用程序是通過消息來驅動的,An...
    一碼立程閱讀 4,491評論 4 36