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

Nếu bạn cần được hỗ trợ về AIDL, hãy xem thêm FMQ với AIDL.

Cơ sở hạ tầng lệnh gọi quy trình từ xa (RPC) của HIDL sử dụng cơ chế Binder, tức là các lệnh gọi liên quan đến mức hao tổn, yêu cầu hoạt động hạt nhân và có thể kích hoạt hành động của bộ lập lịch biểu. Tuy nhiên, đối với trường hợp dữ liệu phải được chuyển giữa các quy trình có chi phí thấp hơn và không liên quan đến hạt nhân, Hàng đợi thông báo nhanh (FMQ) được sử dụng.

FMQ tạo hàng đợi thư với các thuộc tính mong muốn. Một Đối tượng MQDescriptorSync hoặc MQDescriptorUnsync có thể là được gửi qua lệnh gọi HIDL RPC và được quy 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 MessageQueue

Android hỗ trợ 2 loại hàng đợi (được gọi là phiên bản):

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

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

Chưa đồng bộ hóa

Hàng đợi chưa đồng bộ chỉ có một người viết nhưng có thể có bất kỳ số lượng độc giả. Có một vị trí ghi cho hàng đợi; tuy nhiên, mỗi độc giả vẫn muốn theo dõi vị trí đọc độc lập của chính nó.

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

Độc giả chịu trách nhiệm truy xuất dữ liệu trước khi dữ liệu đó kết thúc hàng đợi. Lần đọc cố gắng đọc nhiều dữ liệu hơn mức có sẵn thất bại ngay lập tức (nếu không chặn) hoặc chờ có đủ dữ liệu (nếu chặn). Lần đọc cố gắng đọc nhiều dữ liệu hơn dung lượng của hàng đợi luôn sẽ không thành công ngay lập tức.

Nếu độc giả không nắm bắt được thông tin của tác giả, để lượng dữ liệu được ghi nhưng chưa được độc giả đó đọc lớn hơn dung lượng của hàng đợi, thì lần đọc tiếp theo không trả về dữ liệu; thay vào đó, thao tác này sẽ đặt lại lượt đọc của độc giả để bằng với vị trí ghi gần đây nhất, sau đó 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, dữ liệu cho thấy nhiều dữ liệu cần đọc hơn dung lượng của hàng đợi, cho biết đã xảy ra tràn. (Nếu hàng đợi tràn giữa các lần kiểm tra dữ liệu hiện có và cố đọc dữ liệu đó, dấu hiệu duy nhất của tràn là đọc không thành công).

Những độc giả của Hàng đợi chưa đồng bộ hoá 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ừ trình đọc phần mô tả nên sử dụng đối số "false" cho "resetPointers" .

Đã đồng bộ hoá

Một hàng đợi được đồng bộ hoá gồm một người viết và một trình đọc với một lượt ghi duy nhất và một vị trí đọc duy nhất. Không thể ghi nhiều dữ liệu hơn hàng đợi có không gian cho hoặc đọc nhiều dữ liệu hơn hàng đợi hiện đang lưu giữ. Tuỳ thuộc vào việc hàm ghi hoặc đọc chặn hay không tuần tự đã gọi, cố gắng vượt quá dung lượng có sẵn hoặc dữ liệu trả về lỗi ngay lập tức hoặc chặn cho đến khi thao tác mong muốn có thể được hoàn tất. Nỗ lực đọc hoặc ghi nhiều dữ liệu hơn dung lượng của hàng đợi sẽ luôn gặp lỗi ngay lập tức.

Thiết lập FMQ

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

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

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

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

Phần 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 đầu tiên. Chiến lược phát hành đĩa đơn Đối tượng MQDescriptor được gửi qua lệnh gọi RPC HIDL hoặc AIDL đến quy trình này giữ đầu thứ hai của hàng đợi tin nhắn. Chiến lược phát hành đĩa đơn MQDescriptor chứa thông tin về hàng đợi, bao gồm:

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

Bạn có thể dùng đối tượng MQDescriptor để tạo Đối tượng MessageQueue:

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

Tham số resetPointers cho biết có đặt lại chế độ đọc hay không và ghi vị trí thành 0 trong khi tạo đối tượng MessageQueue này. Trong hàng đợi chưa đồng bộ hoá, vị trí đọc (được cục bộ cho mỗi đối tượng MessageQueue trong hàng đợi chưa đồng bộ hoá) luôn được đặt thành 0 trong quá trình tạo. Thông thường, MQDescriptor được khởi tạo trong tạo đối tượng hàng đợi thông báo đầu tiên. Để có thêm quyền kiểm soát đối với bộ nhớ, bạn có thể thiết lập MQDescriptor theo cách thủ công (MQDescriptor được định nghĩa trong system/libhidl/base/include/hidl/MQDescriptor.h) thì hãy tạo từng đối tượng MessageQueue như 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 lượt đọc/ghi. Có hai loại chặn lệnh gọi đọc/ghi:

  • Dạng ngắn, với 3 tham số (con trỏ dữ liệu, số lượng mục, thời gian chờ). Hỗ trợ chặn từng thao tác đọc/ghi trên một thiết bị 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 nội bộ và đối tượng hàng đợi thông báo đầu tiên phải sẽ được khởi động bằng 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, với 6 thông số (bao gồm cờ sự kiện và mặt nạ bit). Hỗ trợ sử dụng đối tượng EventFlag dùng chung 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 định dạng dài, bạn có thể cung cấp EventFlag một cách rõ ràng trong mỗi lệnh gọi readBlocking()writeBlocking(). Một trong số hàng đợi có thể được khởi chạy bằng một cờ sự kiện nội bộ. Sau đó, cờ này 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à dùng để tạo EventFlag các đối tượng trong mỗi quy trình để sử dụng với các FMQ khác. Ngoài ra, Có thể khởi tạo đối tượng EventFlag bằng bất kỳ đối tượng chia sẻ nào phù hợp bộ nhớ.

Nói chung, mỗi hàng đợi chỉ nên sử dụng một thuộc tính không chặn, dạng ngắn chặn hoặc chặn video dài. Đó không phải là lỗi khi kết hợp chúng, nhưng hãy cẩn thận lập trình quảng cáo để có được kết quả mong muốn.

Đánh dấu kỷ niệm là chỉ có thể đọc

Theo mặc định, bộ nhớ dùng chung có quyền đọc và ghi. Đối với thiết bị chưa được đồng bộ hoá hàng đợi (kUnsynchronizedWrite), người viết có thể muốn xoá quyền ghi cho tất cả số độc giả trước khi đưa ra đối tượng MQDescriptorUnsync. Điều này đảm bảo rằng các giá trị khác các quy trình không thể ghi vào hàng đợi. Bạn nên làm việc này để ngăn chặn lỗi hoặc hành vi xấu trong mà độc giả xử lý. Nếu người viết muốn độc giả có thể đặt lại hàng đợi bất cứ khi nào họ sử dụng MQDescriptorUnsync để tạo phía đọc của hàng đợi, sau đó, hệ thống sẽ không đánh dấu kỷ niệm này ở dạng chỉ đọc. Đây là hành vi mặc định của hàm khởi tạo "MessageQueue". Vì vậy, nếu đã có người dùng hiện tại của hàng đợi này, mã của họ cần được thay đổi để tạo hàng đợi bằng resetPointer=false.

  • Người ghi: gọi ashmem_set_prot_region bằng chỉ số mô tả tệp MQDescriptor và khu vực được đặt thành chỉ đọc (PROT_READ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Trình đọc: tạo hàng đợi thư bằng 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);

Bạn có thể sử dụng availableToWrite()availableToRead() để xác định lượng dữ liệu có thể được chuyển trong một thao tác. Trong một hàng đợi chưa đồng bộ hoá:

  • availableToWrite() luôn trả về sức chứa của hàng đợi.
  • Mỗi độc giả có vị trí đọc riêng và thực hiện phép tính riêng về availableToRead().
  • Theo quan điểm của một trình đọc chậm, hàng đợi được phép tràn; điều này có thể khiến availableToRead() trả về một 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 không thành công và dẫn đến vị trí đọc của trình đọc đó đang được đặt bằng với con trỏ ghi hiện tại, liệu sự cố tràn có được báo cáo qua hay không availableToRead().

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

Các phương thức readBlocking()writeBlocking() đang chờ cho đến khi thao tác được yêu cầu có thể được hoàn tất 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ờ).

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

readBlocking() chờ trên các bit writeNotification; nếu tham số đó là 0 thì cuộc gọi sẽ luôn không thành công. Nếu Giá trị readNotification bằng 0, lệnh gọi không thành công, nhưng đọ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ộ hoá, điều này có nghĩa là lệnh gọi writeBlocking() tương ứng không bao giờ đánh thức trừ phi bit được đặt ở nơi khác. Trong hàng đợi chưa đồng bộ hoá, writeBlocking() không đợi (vẫn nên dùng để đặt giá trị ghi bit thông báo) và phù hợp với các lượt đọc không đặt bất kỳ bit thông báo. Tương tự, writeblocking() sẽ không thành công nếu readNotification là 0 và một lần ghi thành công sẽ đặt giá trị writeNotification bit.

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

Không thực hiện thao tác sao chép

Chiến lược phát hành đĩa đơn read/write/readBlocking/writeBlocking() API lấy con trỏ đến vùng đệm đầu vào/đầu ra làm đối số và sử dụng memcpy() gọi nội bộ để sao chép dữ liệu giữa cùng một Vùng đệm vòng FMQ. Để cải thiện hiệu suất, Android 8.0 trở lên bao gồm một bộ Các API cung cấp quyền truy cập con trỏ trực tiếp vào vùng đệm vòng, loại bỏ cần 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 con trỏ cơ sở vào vòng FMQ vùng đệm. Sau khi ghi dữ liệu, hãy xác nhận dữ liệu bằng commitWrite(). Phương thức beginRead/commitRead hoạt động theo cách tương tự.
  • Phương thức beginRead/Write sẽ làm phương thức nhập số lượng thông báo sẽ được đọc/ghi và trả về một giá trị boolean cho biết liệu giá trị đọc/ghi. Nếu có thể đọc hoặc ghi, memTx cấu trúc được điền sẵn các con trỏ cơ sở có thể dùng cho con trỏ trực tiếp quyền truy cập vào bộ nhớ dùng chung trong vùng đệm đổ chuông.
  • Cấu trúc MemRegion chứa thông tin 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 trong số phần tử T (chiều dài của khối bộ nhớ theo HIDL được xác định loại hàng đợi tin nhắn).
  • Cấu trúc MemTransaction chứa hai MemRegion cấu trúc, firstsecond làm lượt đọc hoặc ghi vào vùng đệm vòng có thể yêu cầu bao bọc quanh đầu hàng đợi. Chiến dịch này có nghĩa là cần 2 con trỏ cơ sở để đọc/ghi dữ liệu vào FMQ vòng đệm.

Cách 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

Để nhận thông tin tham chiếu đến MemRegion đầu tiên và thứ hai trong một Đố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ụ về cách ghi vào FMQ bằ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 ô idx trong MemRegions thuộc MemTransaction này . Nếu đối tượng MemTransaction đang biểu thị 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 copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Ghi nMessages mục thuộc loại T vào vùng bộ nhớ mà đối tượng mô tả, bắt đầu từ chỉ mục startIdx. Phương thức này sử dụng memcpy() và không dành cho trường hợp không có bản sao nào hoạt động. Nếu đối tượng MemTransaction biểu thị bộ nhớ cho đọ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 copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Phương thức trợ giúp để đọc nMessages mục thuộc loại T từ vùng bộ nhớ mà đối tượng mô tả bắt đầu từ startIdx. Chiến dịch này phương thức sử dụng memcpy() và không dùng cho bản sao không hoạt động.

Gửi hàng đợi qua HIDL

Về phía sáng tạo:

  1. Tạo đối tượng hàng đợi thông báo như mô tả ở trên.
  2. Xác minh đối tượng hợp lệ bằng isValid().
  3. Nếu bạn đang đợi nhiều hàng đợi bằng cách chuyển một 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 MessageQueue được khởi tạo để tạo cờ, và hãy 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 description [mô tả].
  5. Trong tệp .hal, hãy cung cấp cho phương thức một tham số thuộc loại fmq_sync hoặc fmq_unsync trong đó T là kiểu xác định HIDL phù hợp. Sử dụng hàm này để gửi đối tượng được trả về getDesc() vào quy trình nhận.

Ở bên nhận:

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