1. 程式人生 > >HIDL學習筆記之HIDL C++(第二天)

HIDL學習筆記之HIDL C++(第二天)

快速訊息佇列 (FMQ)

HIDL 的遠端過程呼叫 (RPC) 基礎架構使用 Binder 機制,這意味著呼叫涉及開銷、需要核心操作,並且可以觸發排程程式操作。

不過,對於必須在開銷較小且無核心參與的程序之間傳輸資料的情況,則使用快速訊息佇列 (FMQ) 系統。

FMQ 會建立具有所需屬性的訊息佇列。MQDescriptorSync 或 MQDescriptorUnsync 物件可通過 HIDL RPC 呼叫傳送,並可供接收程序用於訪問訊息佇列。

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 是在建立第一個訊息佇列物件過程中初始化的。要對共享記憶體進行額外的控制,您可以手動設定 MQDescriptorMQDescriptor 是在 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);

1. availableToWrite() 和 availableToRead() 可用於確定在一次操作中可傳輸的資料量。
在未同步佇列中:

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

2. 如果所有請求的資料都可以(並已)傳輸到佇列/從佇列傳出,則 read() 和 write() 方法會返回 true。這些方法不會阻塞;它們要麼成功(並返回 true),要麼立即返回失敗 (false)。

3. 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() 方法。

零複製操作

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

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 物件傳輸資料。

使用 Binder IPC

從 Android O 開始,Android 框架和 HAL 現在使用 Binder 互相通訊。由於這種通訊方式極大地增加了 Binder 流量,因此 Android O 包含了幾項改進,旨在確保 Binder IPC 的速度。整合最新版 Binder 驅動程式的 SoC 供應商和原始裝置製造商 (OEM) 應該檢視這些改進的列表、用於 3.18、4.4 和 4.9 版核心的相關 SHA,以及所需的使用者空間更改。

多個 Binder 域(上下文)

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

分散-集中

在之前的 Android 版本中,Binder 呼叫中的每條資料都會被複制 3 次:

  • 一次是在呼叫程序中將資料序列化為 Parce
  • 一次是在核心驅動程式中將 Parcel 複製到目標程序
  • 一次是在目標程序中對 Parcel 進行反序列化

Android O 使用分散-集中優化機制將複製次數從 3 次減少到了 1 次。資料保留其原始結構和記憶體佈局,且 Binder 驅動程式會立即將資料複製到目標程序中,而不是先在 Parcel 中對資料進行序列化。在目標程序中,這些資料的結構和記憶體佈局保持不變,並且,在無需再次複製的情況下即可讀取這些資料。

vndbinder

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

IPC 域 說明
/dev/binder 框架/應用程序之間的 IPC,使用 AIDL 介面
dev/hwbinder 框架/供應商程序之間的 IPC,使用 HIDL 介面供應商程序之間的 IPC,使用 HIDL 介面
/dev/vndbinder 供應商/供應商程序之間的 IPC,使用 AIDL 介面

HIDL Memory Block

HIDL 記憶體塊是一個建立在HIDL @1.0::IAllocator, 和 HIDL @1.0::IMapper的抽象層。
它是為具有多個記憶體塊共享單個記憶體堆的HIDL Severis而設計的。

結構

HIDL記憶體塊體系結構包括多個記憶體塊共享一個記憶體堆的HIDL services:
image.png

使用例項

宣告HAL

IFoo HAL:

import [email protected]::MemoryBlock;

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

Android.bp:

hidl_interface {
    ...
    srcs: [
        "IFoo.hal",
    ],
    interfaces: [
        "[email protected]",
        ...
};

實施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.建立一個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申請記憶體

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.使用資料

#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: [
        "[email protected]",

        "[email protected]"

        "[email protected]",
        "libhidlbase",
        "libhidlmemory",

執行緒模型

注意:
標記為 oneway 的方法不會阻塞。對於未標記為 oneway 的方法,在伺服器完成執行任務或呼叫同步回撥(以先發生者為準)之前,客戶端的方法呼叫將一直處於阻塞狀態。
伺服器方法實現最多可以呼叫一個同步回撥;多出的回撥呼叫會被捨棄並記錄為錯誤。如果方法應通過回撥返回值,但未呼叫其回撥,系統會將這種情況記錄為錯誤,並作為傳輸錯誤報告給客戶端。

直通模式下的執行緒

在直通模式下,大多數呼叫都是同步的。不過,為確保 oneway 呼叫不會阻塞客戶端這一預期行為,系統會分別為每個程序建立執行緒。

繫結式 HAL 中的執行緒

為了處理傳入的 RPC 呼叫(包括從 HAL 到 HAL 使用者的非同步回撥)和終止通知,系統會為使用 HIDL 的每個程序關聯一個執行緒池。
如果單個程序實現了多個 HIDL 介面和/或終止通知處理程式,則所有這些介面和/或處理程式會共享其執行緒池。當程序接收從客戶端傳入的方法呼叫時,它會從執行緒池中選擇一個空閒執行緒,並在該執行緒上執行呼叫。如果沒有空閒的執行緒,它將會阻塞,直到有可用執行緒為止。

如果伺服器只有一個執行緒,則傳入伺服器的呼叫將按順序完成。具有多個執行緒的伺服器可以不按順序完成呼叫,即使客戶端只有一個執行緒也是如此

不過,對於特定的介面物件,oneway 呼叫會保證按順序進行(請參閱伺服器執行緒模型。對於託管了多個介面的多執行緒伺服器,對不同介面的多項 oneway 呼叫可能會並行處理,也可能會與其他阻塞呼叫並行處理。

伺服器執行緒模型

(直通模式除外)HIDL 介面的伺服器實現位於不同於客戶端的程序中,並且需要一個或多個執行緒等待傳入的方法呼叫。

這些執行緒構成伺服器的執行緒池;伺服器可以決定它希望在其執行緒池中執行多少執行緒,並且可以利用一個執行緒大小的執行緒池來按順序處理其介面上的所有呼叫。如果伺服器的執行緒池中有多個執行緒,則伺服器可以在其任何介面上接收同時傳入的呼叫(在 C++ 中,這意味著必須小心鎖定共享資料)。

傳入同一介面的單向呼叫會按順序進行處理。如果多執行緒客戶端在介面 IFoo 上呼叫 method1 和 method2,並在介面 IBar 上呼叫 method3,則 method1 和 method2 將始終按順序執行,但 method3 可以與 method1 和 method2 並行執行。

單一客戶端執行執行緒可能會通過以下兩種方式在具有多個執行緒的伺服器上引發並行執行:

  • oneway 呼叫不會阻塞。如果執行 oneway 呼叫,然後呼叫非 oneway,則伺服器可以同時執行 oneway 呼叫和非 oneway 呼叫。
  • 當系統從伺服器呼叫回撥時,通過同步回撥傳回資料的伺服器方法可以立即解除對客戶端的阻塞。

客戶端執行緒模型

非阻塞呼叫(帶有 oneway 關鍵字標記的函式)與阻塞呼叫(未指定 oneway 關鍵字的函式)的客戶端執行緒模型有所不同。

阻塞呼叫

對於阻塞呼叫來說,除非發生以下情況之一,否則客戶端將一直處於阻塞狀態:

  • 出現傳輸錯誤;Return 物件包含可通過 Return::isOk() 檢索的錯誤狀態。
  • 伺服器實現呼叫回撥(如果有)。
  • 伺服器實現返回值(如果沒有回撥引數)。

如果成功的話,客戶端以引數形式傳遞的回撥函式始終會在函式本身返回之前被伺服器呼叫。回撥是在進行函式呼叫的同一執行緒上執行,所以在函式呼叫期間,實現人員必須謹慎地持有鎖(並儘可能徹底避免持有鎖)。不含 generates 語句或 oneway 關鍵字的函式仍處於阻塞狀態;在伺服器返回 Return 物件之前,客戶端將一直處於阻塞狀態。

單向呼叫

如果某個函式標記有 oneway,則客戶端會立即返回,而不會等待伺服器完成其函式呼叫。

資料型別

本節只列舉C++的相關資料型別。

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 hidl_vec 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 };

變為:

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

bitfield

bitfield(其中 T 是使用者定義的列舉)會變為 C++ 形式的該列舉的底層型別。在上述示例中,bitfield 會變為 uint8_t。

vec

hidl_vec 類模板是 libhidlbase 的一部分,可用於傳遞具備任意大小的任何 HIDL 型別的向量。與之相當的具有固定大小的容器是 hidl_array。此外,您也可以使用 hidl_vec::setToExternal() 函式將 hidl_vec 初始化為指向 T 型別的外部資料緩衝區。

除了在生成的 C++ 標頭檔案中適當地發出/插入結構之外,您還可以使用 vec 生成一些便利函式,用於轉換到 std::vector 和 T 裸指標或從它們進行轉換。如果您將 vec 用作引數,則使用它的函式將過載(將生成兩個原型),以接受並傳遞該引數的 HIDL 結構和 std::vector 型別。

陣列

hidl 中的常量陣列由 libhidlbase 中的 hidl_array 類表示。hidl_array<T, S1, S2, …, SN> 表示具有固定大小的 N 維陣列 T[S1][S2]…[SN]。

字串

hidl_string 類(libhidlbase 的一部分)可用於通過 HIDL 介面傳遞字串,並在 /system/libhidl/base/include/hidl/HidlSupport.h 下進行定義。該類中的第一個儲存位置是指向其字元緩衝區的指標。

struct

HIDL 形式的 struct 只能包含固定大小的資料型別,不能包含任何函式。HIDL 結構定義會直接對映到 C++ 形式的標準佈局 struct,從而確保 struct 具有一致的記憶體佈局。一個struct可以包括多種指向單獨的可變長度緩衝區的 HIDL 型別(包括 handle、string 和 vec)。

handle

handle 型別由 C++ 形式的 hidl_handle 結構表示,該結構是一個簡單的封裝容器,用於封裝指向 const native_handle_t 物件的指標(該物件已經在 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 類,該類表示未對映的共享記憶體。這是要在 HIDL 中共享記憶體而必須在程序之間傳遞的物件。要使用共享記憶體,需滿足以下條件:
1.獲取 IAllocator 的例項(當前只有“ashmem”例項可用)並使用該例項分配共享記憶體。
2.IAllocator::allocate() 返回 hidl_memory 物件,該物件可通過 HIDL RPC 傳遞,並能使用 libhidlmemory 的 mapMemory 函式對映到某個程序。
3.mapMemory 返回對可用於訪問記憶體的 sp 物件的引用(IMemory 和 IAllocator 在 [email protected] 中定義)。

IAllocator 的例項可用於分配記憶體:

#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
  }));

對記憶體的實際更改必須通過 IMemory 物件完成(在建立 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();

介面

介面可作為物件傳遞。“介面”一詞可用作 [email protected]::IBase 型別的語法糖;此外,當前的介面以及任何匯入的介面都將定義為一個型別。

儲存介面的變數應該是強指標:sp。接受介面引數的 HIDL 函式會將原始指標轉換為強指標,從而導致不可預料的行為(可能會意外清除指標)。為避免出現問題,請務必將 HIDL 介面儲存為 sp<>。

建立 HAL 客戶端

首先將 HAL 庫新增到 makefile 中:

Make:LOCAL_SHARED_LIBRARIES += [email protected]
Soong:shared_libs: [ …, [email protected] ]

接下來,新增 HAL 標頭檔案:

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

建立 HAL 伺服器

要建立 HAL 實現,您必須具有表示 HAL 的 .hal 檔案並已在 hidl-gen 上使用 -Lmakefile 或 -Landroidbp 為 HAL 生成 makefile(./hardware/interfaces/update-makefiles.sh 會為內部 HAL 檔案執行這項操作,這是一個很好的參考)。從 libhardware 通過 HAL 傳輸時,您可以使用 c2hal 輕鬆完成許多此類工作。

要建立必要的檔案來實現您的 HAL,請使用以下程式碼:

[email protected]
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 庫執行 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"(例如 [email protected])中,但也可以位於任何位置。HAL 的特定類的 sepolicy 是屬性 hal_<module>(例如 hal_nfc))。您必須將此屬性應用到執行特定 HAL 的守護程序(如果同一程序提供多個 HAL,則可以將多個屬性應用到該程序)。

軟體包

HIDL 介面軟體包位於 hardware/interfaces 或 vendor/ 目錄下(少數例外情況除外)。hardware/interfaces 頂層會直接對映到 android.hardware 軟體包名稱空間;版本是軟體包(而不是介面)名稱空間下的子目錄。

hidl-gen 編譯器會將 .hal 檔案編譯成一組 .h 和 .cpp 檔案。這些自動生成的檔案可用來編譯客戶端/伺服器實現連結到的共享庫。用於編譯此共享庫的 Android.bp 檔案由 hardware/interfaces/update-makefiles.sh 指令碼自動生成。每次將新軟體包新增到 hardware/interfaces 或在現有軟體包中新增/移除 .hal 檔案時,您都必須重新執行該指令碼,以確保生成的共享庫是最新的。

例如,IFoo.hal 示例檔案應該位於 hardware/interfaces/samples/1.0 下。IFoo.hal 示例檔案可以在 samples 軟體包中建立一個 IFoo 介面:

package [email protected];
interface IFoo {
    struct Foo {
       int64_t someValue;
       handle  myHandle;
    };

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

生成的檔案

HIDL 軟體包中自動生成的檔案會連結到與軟體包同名的單個共享庫(例如 [email protected])。該共享庫還會匯出單個標頭 IFoo.h,用於包含在客戶端和伺服器中。繫結式模式使用 hidl-gen 編譯器並以 IFoo.hal 介面檔案作為輸入,它具有以下自動生成的檔案:
image.png

連結到共享庫

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

LOCAL_SHARED_LIBRARIES += [email protected]

在 Android.bp 中:

shared_libs: [
    /* ... */
    "[email protected]",
],

image.png

名稱空間

HIDL 函式和型別(如 Return 和 Void())已在名稱空間 ::android::hardware 中進行宣告。軟體包的 C++ 名稱空間由軟體包的名稱和版本號確定。例如,hardware/interfaces 下版本為 1.2 的軟體包 mypackage 具有以下特質:

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

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

相關推薦

HIDL學習筆記HIDL C++第二

快速訊息佇列 (FMQ) HIDL 的遠端過程呼叫 (RPC) 基礎架構使用 Binder 機制,這意味著呼叫涉及開銷、需要核心操作,並且可以觸發排程程式操作。 不過,對於必須在開銷較小且無核心參與的程序之間傳輸資料的情況,則使用快速訊息佇列 (FMQ) 系統。

學習筆記計算機網路王道考研 第二章 物理層

模擬資料(模擬訊號)和離散資料(離散訊號) 序列傳輸和並行傳輸 基帶訊號:將數字訊號1和0直接用兩種不同的電壓表示,然後傳送到數字通道上去傳輸(稱為基帶傳輸) 寬頻訊號:將基帶訊號進行調製後形成的頻分複用模擬訊號,然後傳送到模擬通道上去傳輸(稱為寬頻傳輸) 單工通訊(僅

學習筆記計算機網路王道考研 第六章 應用層

在C/S模型中,伺服器總是處於開啟狀態(除非某人把它關了) 常見的使用C/S模型的英應用包括Web、檔案傳輸(FTP)、遠端登入和電子郵件等  C/S模型的主要特點: 網路中各計算機的地位不平等,伺服器可以通過對使用者許可權的限制來達到管理客戶機的目的

學習筆記計算機網路王道考研 第五章 傳輸層

傳輸層屬於面向通訊的最高層,同時也是使用者功能中的最低層 傳輸層提供應用程序之間的邏輯通訊(即端到端的通訊),與網路層的區別是,網路層提供的是主機之間的邏輯通訊 傳輸層的複用和分用:複用是指傳送方不同的應用程序都可以使用同一個傳輸層協議傳送資料;分用是指接收方的傳輸層在剝去報文的首

學習筆記計算機網路王道考研 第四章 網路層

網路層的功能: 異構網路互聯 路由與轉發。路由表的兩大功能:路由選擇和分組轉發 擁塞控制 判斷網路是否進入擁塞狀態的方法是觀察網路的吞吐量與網路的負載的關係:如果隨著網路負載的增加,網路的吞吐量明顯小於正常的吞吐量,那麼網路就可能進入了“輕度擁塞狀態”;

學習筆記計算機網路王道考研 第三章 資料鏈路層

資料鏈路層的功能: 為網路層提供服務 鏈路管理 幀定界、幀同步和透明傳輸 流量控制 差錯控制 資料鏈路層可以為網路層提供的服務有: 無確認的無連線服務 有確認的無連線服務 有確認的面向連線服務(有連線則一定有確認)

學習筆記計算機網路王道考研 第一章 計算機網路體系結構

計算機網路是一些互聯的、自制的計算機系統的集合 計算機網路的組成: 從組成部分看,計算機網路主要由硬體、軟體和協議組成 從工作方式看,計算機網路可分為和邊緣部分核心部分。邊緣部分由供使用者直接使用的主機組成,核心部分由大量的網路和連線這些網路的路由器組成 從功能

完整學習筆記Android基礎詳版

Android專案的目錄結構(熟悉) Activity:應用被開啟時顯示的介面 src:專案程式碼 R.java:專案中所有資原始檔的資源id Android.jar:Android的jar包,匯入此包方可使用Android的api libs:匯入第三方ja

TensorFlow學習筆記疑問解答持續更新

1、tensorflow中一箇中括號和兩個中括號是什麼意思? b = tf.constant([3,3]) c = tf.constant([[3,3]]) with tf.Session() as sess: print(b,c) pri

C++標準模板庫學習筆記序列容器vector、array

序列容器以線性序列的方式儲存元素。五種標準的序列容器:array<T,N>,vector<T>,deque<T>,list<T>,forward_list<T>。Arrayarray<T, N>是一個有N

設計模式C++學習筆記十一c/c++面試筆試題

一、指標與引用有什麼區別? 1、指標會佔用記憶體,引用不佔用記憶體。 2、引用在定義時必須初始化。 3、沒有空的引用,但是有空的指標。 二、static關鍵的幾個作用 1、函式體內的static變數的作用範圍為該函式體,該變數記憶體只分配一次,因此其值在下次再呼叫該函式時

【黑馬程式設計師】Objective-C語言學習筆記核心語法

--------------------------------------------IOS期待與您交流!-------------------------------------------- 一、點語法 1、沒有使用點語法的情況 此時我們使用setter和gette

JavaSE 學習筆記接 口

之前 rac 關鍵字 extends 修飾符 對象 clas con 而且 接 口: 1:是用關鍵字interface定義的。 2:接口中包含的成員,最常見的有全局常量、抽象方法。 註意:接口中的成員都有固定的修飾符。 成員變量:public static fina

JavaSE 學習筆記多態

會有 轉換 容易 per 不同 如何 person 特點 一句話 多 態:函數本身就具備多態性,某一種事物有不同的具體的體現。 體現:父類引用或者接口的引用指向了自己的子類對象。//Animal a = new Cat(); 多態的好處:提高了程序的擴展性。 多態的弊端

JavaSE 學習筆記Java概述

environ 電子 6.0 run javase 有一點 架構 spa form 一、Java的三種技術架構: JAVAEE:Java Platform Enterprise Edition,開發企業環境下的應用程序,主要針對web程序開發; JAVASE:Java P

matlab學習筆記常用命令

plot 我們 all 查看 學習 ear tla clear 但是 一.清除命令。   1.clear all;%清除所有變量,通常在matlab的工作區;另外斷點也會被清除掉   2.close all;%關閉所有窗口(除了編輯器窗口、命令窗口、幫助窗口)   3.cl

java學習筆記基礎語法

讓其 實例 高效率 使用 個數 存儲 記錄 棧內存 數組 1.數組: 概念:同一種類型數據的集合,其實,數組就是一個容器 優點:可以方便的對其進行操作,編號從0開始,方便操作這些元素。 2,數組的格式 元素類型[]數組名=new 元素類型[數組元素個

【itext學習路】-------第二設定pdf的一些常用屬性

在上一篇文章中,我們已經成功的建立了一個簡單的pdf,下面我將學習設定該pdf的常用屬性,其中包括:作者,建立時間,pdf建立者,pdf生產者,關鍵字,標題,主題 下面是我們的程式碼,非常簡單。 package cn.tomtocc.pdf; imp

設計模式學習筆記09--代理模式動態代理

1.動態代理     動態代理還是屬於設計模式--代理模式的一種,代理類在程式執行時建立的代理方式被成為動態代理。動態代理是在實現階段不用關心代理誰,而在執行階段才指定代理哪一個物件。相對來說,自己寫代理類的方式就是靜態代理。現在有一個非常流行的名稱叫做面向橫切面程式設計,也

linux學習筆記shell程式設計

shell程式設計 基礎正則表示式 正則和萬用字元的區別:正則是包含匹配,匹配檔案內容,grep,awk等支援正則表示式。萬用字元是完全匹配,匹配檔名,例如find,ls不認識正則表示式 ####正則