Hàng đợi tin nhắn nhanh (FMQ)

Nếu bạn đang tìm kiếm hỗ trợ AIDL, hãy xem thêm FMQ với AIDL .

Cơ sở hạ tầng cuộc gọi thủ tục từ xa (RPC) của HIDL sử dụng cơ chế Binder, nghĩa là các cuộc gọi liên quan đến chi phí chung, yêu cầu hoạt động của kernel và có thể kích hoạt hành động lập lịch. Tuy nhiên, đối với các trường hợp dữ liệu phải được truyền giữa các tiến trình với ít chi phí hơn và không có sự tham gia của kernel, hệ thống Hàng đợi tin nhắn nhanh (FMQ) sẽ được sử dụng.

FMQ tạo hàng đợi tin nhắn với các thuộc tính mong muốn. Một đối tượng MQDescriptorSync hoặc MQDescriptorUnsync có thể được gửi qua cuộc gọi HIDL RPC và được quá trình nhận sử dụng để truy cập vào hàng đợi tin nhắn.

Hàng đợi tin nhắn nhanh chỉ được hỗ trợ trong C++ và trên các thiết bị chạy Android 8.0 trở lên.

Các loại hàng đợi tin nhắn

Android hỗ trợ hai loại hàng đợi (được gọi là hương vị ):

  • Hàng đợi không đồng bộ được phép tràn và có thể có nhiều người đọc; mỗi đầu đọc phải đọc dữ liệu kịp thời nếu không sẽ mất dữ liệu.
  • Hàng đợi được đồng bộ hóa không được phép tràn và chỉ có thể có một đầu đọc.

Cả hai loại hàng đợi đều không được phép tràn (đọc từ hàng đợi trống sẽ không thành công) và chỉ có thể có một người ghi.

Không đồng bộ

Hàng đợi không đồng bộ chỉ có một người ghi nhưng có thể có số lượng người đọc bất kỳ. Có một vị trí ghi cho hàng đợi; tuy nhiên, mỗi đầu đọc sẽ theo dõi vị trí đọc độc lập của riêng mình.

Việc ghi vào hàng đợi luôn thành công (không được kiểm tra tràn) miễn là chúng không lớn hơn dung lượng hàng đợi đã định cấu hình (việc ghi lớn hơn dung lượng hàng đợi sẽ không thành công ngay lập tức). Vì mỗi đầu đọc có thể có một vị trí đọc khác nhau, thay vì đợi mọi đầu đọc đọc từng phần dữ liệu, dữ liệu được phép rời khỏi hàng đợi bất cứ khi nào thao tác ghi mới cần dung lượng trống.

Người đọc có trách nhiệm truy xuất dữ liệu trước khi nó rơi ra khỏi cuối hàng đợi. Quá trình đọc cố gắng đọc nhiều dữ liệu hơn mức có sẵn sẽ không thành công ngay lập tức (nếu không chặn) hoặc chờ có đủ dữ liệu (nếu chặn). Một lần đọc cố gắng đọc nhiều dữ liệu hơn dung lượng hàng đợi luôn thất bại ngay lập tức.

Nếu đầu đọc không theo kịp đầu ghi, khiến lượng dữ liệu được ghi và chưa đọc của đầu đọc đó lớn hơn dung lượng hàng đợi thì lần đọc tiếp theo không trả về dữ liệu; thay vào đó, nó đặt lại vị trí đọc của đầu đọc bằng vị trí ghi mới nhất rồi trả về lỗi. Nếu dữ liệu có sẵn để đọc được kiểm tra sau khi tràn nhưng trước lần đọc tiếp theo, nó sẽ hiển thị nhiều dữ liệu có sẵn để đọc hơn dung lượng hàng đợi, cho biết đã xảy ra tràn. (Nếu hàng đợi tràn giữa việc kiểm tra dữ liệu có sẵn và cố gắng đọc dữ liệu đó, dấu hiệu tràn duy nhất là quá trình đọc không thành công.)

Người đọc hàng đợi Không đồng bộ có thể không muốn đặt lại con trỏ đọc và ghi của hàng đợi. Vì vậy, khi tạo hàng đợi từ bộ mô tả, trình đọc nên sử dụng đối số `false` cho tham số `resetPointers`.

Đã đồng bộ hóa

Hàng đợi được đồng bộ hóa có một người ghi và một người đọc với một vị trí ghi và một vị trí đọc. Không thể ghi nhiều dữ liệu hơn số lượng mà hàng đợi có hoặc đọc nhiều dữ liệu hơn số lượng mà hàng đợi hiện có. Tùy thuộc vào việc chức năng ghi hoặc đọc chặn hay không chặn được gọi, các nỗ lực vượt quá dung lượng có sẵn hoặc dữ liệu sẽ trả về lỗi ngay lập tức hoặc chặn cho đến khi có thể hoàn thành thao tác mong muốn. Những nỗ lực đọc hoặc ghi nhiều dữ liệu hơn dung lượng hàng đợi sẽ luôn thất bại ngay lập tức.

Thiết lập FMQ

Hàng đợi tin nhắn yêu cầu nhiều đối tượng MessageQueue : một đối tượng được ghi vào và một hoặc nhiều đối tượng được đọc từ đó. Không có cấu hình rõ ràng về đối tượng nào được sử dụng để viết hoặc đọc; Người dùng tùy thuộc vào việc đảm bảo rằng không có đối tượng nào được sử dụng cho cả đọc và viết, có nhiều nhất một trình ghi và đối với các hàng đợi được đồng bộ hóa, có nhiều nhất một trình đọc.

Tạo đối tượng MessageQueue đầu tiên

Hàng đợi tin nhắn được tạo và định cấu hình chỉ bằng một cuộc gọi:

#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 */);
  • Trình khởi tạo MessageQueue<T, flavor>(numElements) tạo và khởi tạo một đối tượng hỗ trợ chức năng hàng đợi tin nhắn.
  • Trình khởi tạo MessageQueue<T, flavor>(numElements, configureEventFlagWord) tạo và khởi tạo một đối tượng hỗ trợ chức năng xếp hàng tin nhắn với tính năng chặn.
  • flavor có thể là kSynchronizedReadWrite cho hàng đợi được đồng bộ hóa hoặc kUnsynchronizedWrite cho hàng đợi không được đồng bộ hóa.
  • uint16_t (trong ví dụ này) có thể là bất kỳ loại nào do HIDL xác định không liên quan đến bộ đệm lồng nhau (không có loại string hoặc vec ), bộ điều khiển hoặc giao diện.
  • kNumElementsInQueue cho biết kích thước của hàng đợi theo số lượng mục nhập; nó xác định kích thước của bộ nhớ đệm dùng chung sẽ được phân bổ cho hàng đợi.

Tạo đối tượng MessageQueue thứ hai

Mặt thứ hai của hàng đợi tin nhắn được tạo bằng cách sử dụng đối tượng MQDescriptor thu được từ mặt thứ nhất. Đối tượng MQDescriptor được gửi qua lệnh gọi RPC HIDL hoặc AIDL tới quy trình sẽ giữ đầu thứ hai của hàng đợi tin nhắn. MQDescriptor chứa thông tin về hàng đợi, bao gồm:

  • Thông tin để ánh xạ bộ đệm và ghi con trỏ.
  • Thông tin để ánh xạ con trỏ đọc (nếu hàng đợi được đồng bộ hóa).
  • Thông tin để ánh xạ từ cờ sự kiện (nếu hàng đợi đang chặn).
  • Loại đối tượng ( <T, flavor> ), bao gồm loại phần tử hàng đợi do HIDL xác định và loại hàng đợi (được đồng bộ hóa hoặc không đồng bộ hóa).

Đối tượng MQDescriptor có thể được sử dụng để xây dựng đối tượng MessageQueue :

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

Tham số resetPointers cho biết có nên đặt lại vị trí đọc và ghi về 0 trong khi tạo đối tượng MessageQueue này hay không. Trong hàng đợi không đồng bộ, vị trí đọc (cục bộ của từng đối tượng MessageQueue trong hàng đợi không đồng bộ) luôn được đặt thành 0 trong quá trình tạo. Thông thường, MQDescriptor được khởi tạo trong quá trình tạo đối tượng hàng đợi tin nhắn đầu tiên. Để kiểm soát thêm bộ nhớ dùng chung, bạn có thể thiết lập MQDescriptor theo cách thủ công ( MQDescriptor được xác định trong system/libhidl/base/include/hidl/MQDescriptor.h ) sau đó tạo mọi đối tượng MessageQueue như được mô tả trong phần này.

Chặn hàng đợi và cờ sự kiện

Theo mặc định, hàng đợi không hỗ trợ chặn đọc/ghi. Có hai loại chặn cuộc gọi đọc/ghi:

  • Dạng ngắn , có ba tham số (con trỏ dữ liệu, số mục, thời gian chờ). Hỗ trợ chặn các hoạt động đọc/ghi riêng lẻ trên một hàng đợi. Khi sử dụng biểu mẫu này, hàng đợi sẽ xử lý cờ sự kiện và mặt nạ bit bên trong và đối tượng hàng đợi tin nhắn đầu tiên phải được khởi tạo với tham số thứ hai là true . Ví dụ:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Dạng dài , có sáu tham số (bao gồm cờ sự kiện và mặt nạ bit). Hỗ trợ sử dụng đối tượng EventFlag được chia sẻ giữa nhiều hàng đợi và cho phép chỉ định mặt nạ bit thông báo sẽ được sử dụng. Trong trường hợp này, cờ sự kiện và mặt nạ bit phải được cung cấp cho mỗi lệnh gọi đọc và ghi.

Đối với dạng dài, EventFlag có thể được cung cấp rõ ràng trong mỗi lệnh gọi readBlocking()writeBlocking() . Một trong các hàng đợi có thể được khởi tạo bằng cờ sự kiện nội bộ, sau đó phải được trích xuất từ ​​các đối tượng MessageQueue của hàng đợi đó bằng cách sử dụng getEventFlagWord() và được sử dụng để tạo các đối tượng EventFlag trong mỗi quy trình để sử dụng với các FMQ khác. Ngoài ra, các đối tượng EventFlag có thể được khởi tạo với bất kỳ bộ nhớ chia sẻ phù hợp nào.

Nói chung, mỗi hàng đợi chỉ nên sử dụng một trong các loại không chặn, chặn dạng ngắn hoặc chặn dạng dài. Việc trộn chúng không phải là lỗi, nhưng cần phải lập trình cẩn thận để có được kết quả mong muốn.

Đánh dấu bộ nhớ là chỉ đọc

Theo mặc định, bộ nhớ dùng chung có quyền đọc và ghi. Đối với hàng đợi không đồng bộ hóa ( kUnsynchronizedWrite ), người viết có thể muốn xóa quyền ghi đối với tất cả người đọc trước khi đưa ra các đối tượng MQDescriptorUnsync . Điều này đảm bảo các quy trình khác không thể ghi vào hàng đợi. Điều này được khuyến nghị để bảo vệ khỏi lỗi hoặc hành vi xấu trong quy trình đọc. Nếu người viết muốn người đọc có thể đặt lại hàng đợi bất cứ khi nào họ sử dụng MQDescriptorUnsync để tạo mặt đọc của hàng đợi thì bộ nhớ không thể được đánh dấu là chỉ đọc. Đây là hành vi mặc định của hàm tạo `MessageQueue`. Vì vậy, nếu đã có người dùng hiện tại của hàng đợi này thì mã của họ cần được thay đổi để tạo hàng đợi với resetPointer=false .

  • Người viết: gọi ashmem_set_prot_region bằng bộ mô tả tệp MQDescriptor và vùng được đặt thành chỉ đọc ( PROT_READ ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Reader: tạo hàng đợi tin nhắn với resetPointer=false (mặc định là true ):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Sử dụng MessageQueue

API công khai của đối tượng MessageQueue là:

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() có thể được sử dụng để xác định lượng dữ liệu có thể được truyền trong một thao tác. Trong hàng đợi không đồng bộ:

  • availableToWrite() luôn trả về dung lượng của hàng đợi.
  • Mỗi đầu đọc có vị trí đọc riêng và thực hiện phép tính riêng cho availableToRead() .
  • Theo quan điểm của người đọc chậm, hàng đợi được phép tràn; điều này có thể dẫn đến availableToRead() trả về giá trị lớn hơn kích thước của hàng đợi. Lần đọc đầu tiên sau khi tràn sẽ không thành công và dẫn đến vị trí đọc của trình đọc đó được đặt bằng với con trỏ ghi hiện tại, cho dù lỗi tràn có được báo cáo thông qua availableToRead() hay không.

Các phương thức read()write() trả về true nếu tất cả dữ liệu được yêu cầu có thể (và đã) được chuyển đến/từ hàng đợi. Những phương pháp này không chặn; họ có thể thành công (và trả về true ) hoặc trả về thất bại ( false ) ngay lập tức.

Các phương thức readBlocking()writeBlocking() đợi cho đến khi thao tác được yêu cầu có thể được hoàn thành hoặc cho đến khi chúng hết thời gian chờ (giá trị timeOutNanos bằng 0 có nghĩa là không bao giờ hết thời gian chờ).

Hoạt động chặn được thực hiện bằng cách sử dụng từ cờ sự kiện. Theo mặc định, mỗi hàng đợi tạo và sử dụng từ gắn cờ riêng để hỗ trợ dạng ngắn của readBlocking()writeBlocking() . Nhiều hàng đợi có thể chia sẻ một từ duy nhất, do đó một quá trình có thể chờ ghi hoặc đọc vào bất kỳ hàng đợi nào. Có thể lấy một con trỏ tới từ cờ sự kiện của hàng đợi bằng cách gọi getEventFlagWord() và con trỏ đó (hoặc bất kỳ con trỏ nào tới vị trí bộ nhớ dùng chung phù hợp) có thể được sử dụng để tạo một đối tượng EventFlag để chuyển sang dạng dài readBlocking()writeBlocking() cho một hàng đợi khác. Các tham số readNotificationwriteNotification cho biết bit nào trong cờ sự kiện sẽ được sử dụng để báo hiệu việc đọc và ghi trên hàng đợi đó. readNotificationwriteNotification là các mặt nạ bit 32 bit.

readBlocking() chờ các bit writeNotification ; nếu tham số đó bằng 0 thì cuộc gọi luôn thất bại. Nếu giá trị readNotification là 0, cuộc gọi sẽ không thất bại nhưng việc đọc thành công sẽ không đặt bất kỳ bit thông báo nào. Trong hàng đợi được đồng bộ hóa, điều này có nghĩa là lệnh gọi writeBlocking() tương ứng sẽ không bao giờ thức dậy trừ khi bit được đặt ở nơi khác. Trong hàng đợi không đồng bộ, writeBlocking() sẽ không chờ (nó vẫn nên được sử dụng để đặt bit thông báo ghi) và việc đọc không đặt bất kỳ bit thông báo nào là phù hợp. Tương tự, writeblocking() sẽ thất bại nếu readNotification bằng 0 và việc ghi thành công sẽ đặt các bit writeNotification được chỉ định.

Để chờ trên nhiều hàng đợi cùng một lúc, hãy sử dụng phương thức wait() của đối tượng EventFlag để chờ trên một mặt nạ thông báo. Phương thức wait() trả về một từ trạng thái có các bit gây ra thiết lập đánh thức. Thông tin này sau đó được sử dụng để xác minh hàng đợi tương ứng có đủ dung lượng hoặc dữ liệu cho thao tác ghi/đọc mong muốn và thực hiện write() / read() không chặn. Để nhận được thông báo về hoạt động đăng bài, hãy sử dụng một lệnh gọi khác đến phương thức wake() của EventFlag . Để biết định nghĩa về tính trừu tượng EventFlag , hãy tham khảo system/libfmq/include/fmq/EventFlag.h .

Không có hoạt động sao chép

Các API read / write / readBlocking / writeBlocking() lấy con trỏ tới bộ đệm đầu vào/đầu ra làm đối số và sử dụng lệnh gọi memcpy() nội bộ để sao chép dữ liệu giữa bộ đệm đó và bộ đệm vòng FMQ. Để cải thiện hiệu suất, Android 8.0 trở lên bao gồm một bộ API cung cấp quyền truy cập con trỏ trực tiếp vào bộ đệm vòng, loại bỏ nhu cầu sử dụng lệnh gọi memcpy .

Sử dụng các API công khai sau đây cho các hoạt động FMQ không sao chép:

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);
  • Phương thức beginWrite cung cấp các con trỏ cơ sở vào bộ đệm vòng FMQ. Sau khi dữ liệu được ghi, hãy cam kết nó bằng cách sử dụng commitWrite() . Các phương thức beginRead / commitRead hoạt động theo cách tương tự.
  • Các phương thức beginRead / Write lấy số lượng tin nhắn cần đọc/ghi làm đầu vào và trả về một boolean cho biết liệu việc đọc/ghi có thể thực hiện được hay không. Nếu có thể đọc hoặc ghi thì cấu trúc memTx được điền với các con trỏ cơ sở có thể được sử dụng để truy cập con trỏ trực tiếp vào bộ nhớ chia sẻ bộ đệm vòng.
  • Cấu trúc MemRegion chứa các chi tiết về một khối bộ nhớ, bao gồm con trỏ cơ sở (địa chỉ cơ sở của khối bộ nhớ) và độ dài tính theo T (độ dài của khối bộ nhớ theo loại hàng đợi tin nhắn do HIDL xác định).
  • Cấu trúc MemTransaction chứa hai cấu trúc MemRegion , firstsecond khi đọc hoặc ghi vào bộ đệm vòng có thể yêu cầu bao quanh phần đầu của hàng đợi. Điều này có nghĩa là cần có hai con trỏ cơ sở để đọc/ghi dữ liệu vào bộ đệm vòng FMQ.

Để lấy địa chỉ cơ sở và độ dài từ cấu trúc 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

Để lấy tham chiếu đến MemRegion thứ nhất và thứ hai trong đối tượng MemTransaction :

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

Ví dụ ghi vào FMQ bằng cách sử dụng API không sao chép:

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
}

Các phương thức trợ giúp sau đây cũng là một phần của MemTransaction :

  • T* getSlot(size_t idx);
    Trả về một con trỏ tới vị trí idx trong MemRegions là một phần của đối tượng MemTransaction này. Nếu đối tượng MemTransaction đại diện cho các vùng bộ nhớ để đọc/ghi N mục thuộc loại T thì phạm vi hợp lệ của idx là từ 0 đến N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Viết các mục nMessages loại T vào các vùng bộ nhớ được mô tả bởi đối tượng, bắt đầu từ chỉ mục startIdx . Phương pháp này sử dụng memcpy() và không nhằm mục đích sử dụng cho thao tác không sao chép. Nếu đối tượng MemTransaction đại diện cho bộ nhớ để đọc/ghi N mục thuộc loại T thì phạm vi hợp lệ của idx nằm trong khoảng từ 0 đến N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Phương thức trợ giúp để đọc các mục nMessages thuộc loại T từ các vùng bộ nhớ được mô tả bởi đối tượng bắt đầu từ startIdx . Phương pháp này sử dụng memcpy() và không nhằm mục đích sử dụng cho thao tác không sao chép.

Gửi hàng đợi qua HIDL

Về phía tạo:

  1. Tạo đối tượng hàng đợi tin nhắn như mô tả ở trên.
  2. Xác minh đối tượng hợp lệ với isValid() .
  3. Nếu bạn sẽ đợi trên nhiều hàng đợi bằng cách chuyển EventFlag sang dạng dài readBlocking() / writeBlocking() , bạn có thể trích xuất con trỏ cờ sự kiện (sử dụng getEventFlagWord() ) từ một đối tượng MessageQueue đã được khởi tạo để tạo cờ, và sử dụng cờ đó để tạo đối tượng EventFlag cần thiết.
  4. Sử dụng phương thức MessageQueue getDesc() để lấy đối tượng mô tả.
  5. Trong tệp .hal , cung cấp cho phương thức một tham số loại fmq_sync hoặc fmq_unsync trong đó T là loại được xác định bởi HIDL phù hợp. Sử dụng điều này để gửi đối tượng được trả về bởi getDesc() tới quá trình nhận.

Về phía nhận:

  1. Sử dụng đối tượng mô tả để tạo đối tượng MessageQueue . Hãy đảm bảo sử dụng cùng loại dữ liệu và kiểu hàng đợi, nếu không mẫu sẽ không biên dịch được.
  2. Nếu bạn trích xuất cờ sự kiện, hãy trích xuất cờ từ đối tượng MessageQueue tương ứng trong quá trình nhận.
  3. Sử dụng đối tượng MessageQueue để truyền dữ liệu.