Antrian Pesan Cepat (FMQ)

Infrastruktur panggilan prosedur jarak jauh (RPC) HIDL menggunakan mekanisme Binder, artinya panggilan melibatkan overhead, memerlukan operasi kernel, dan dapat memicu tindakan penjadwal. Namun, untuk kasus di mana data harus ditransfer antar proses dengan overhead yang lebih sedikit dan tanpa keterlibatan kernel, sistem Fast Message Queue (FMQ) digunakan.

FMQ membuat antrian pesan dengan properti yang diinginkan. MQDescriptorSync atau MQDescriptorUnsync dapat dikirim melalui panggilan HIDL RPC dan digunakan oleh proses penerima untuk mengakses antrian pesan.

Antrean Pesan Cepat hanya didukung di C++ dan pada perangkat yang menjalankan Android 8.0 dan lebih tinggi.

Jenis Antrian Pesan

Android mendukung dua jenis antrian (dikenal sebagai flavor ):

  • Antrian yang tidak disinkronkan dibiarkan meluap, dan dapat memiliki banyak pembaca; setiap pembaca harus membaca data tepat waktu atau kehilangannya.
  • Antrian yang disinkronkan tidak boleh meluap, dan hanya dapat memiliki satu pembaca.

Kedua jenis antrian tidak boleh underflow (membaca dari antrian kosong akan gagal) dan hanya dapat memiliki satu penulis.

Tidak disinkronkan

Antrian yang tidak disinkronkan hanya memiliki satu penulis, tetapi dapat memiliki sejumlah pembaca. Ada satu posisi tulis untuk antrian; namun, setiap pembaca melacak posisi membaca independennya sendiri.

Penulisan ke antrean selalu berhasil (tidak diperiksa untuk luapan) selama tidak lebih besar dari kapasitas antrean yang dikonfigurasi (penulisan lebih besar dari kapasitas antrean langsung gagal). Karena setiap pembaca mungkin memiliki posisi baca yang berbeda, daripada menunggu setiap pembaca membaca setiap bagian data, data dibiarkan keluar dari antrian setiap kali penulisan baru membutuhkan ruang.

Baca bertanggung jawab untuk mengambil data sebelum jatuh dari akhir antrian. Pembacaan yang mencoba membaca lebih banyak data daripada yang tersedia akan langsung gagal (jika tidak memblokir) atau menunggu data yang cukup tersedia (jika memblokir). Pembacaan yang mencoba membaca lebih banyak data daripada kapasitas antrian selalu langsung gagal.

Jika pembaca gagal mengikuti penulis, sehingga jumlah data yang ditulis dan belum dibaca oleh pembaca itu lebih besar dari kapasitas antrian, pembacaan berikutnya tidak mengembalikan data; alih-alih, ini mengatur ulang posisi baca pembaca agar sama dengan posisi tulis terakhir lalu mengembalikan kegagalan. Jika data yang tersedia untuk dibaca diperiksa setelah luapan tetapi sebelum pembacaan berikutnya, ini menunjukkan lebih banyak data yang tersedia untuk dibaca daripada kapasitas antrian, yang menunjukkan telah terjadi luapan. (Jika antrian meluap antara memeriksa data yang tersedia dan mencoba membaca data itu, satu-satunya indikasi meluap adalah bahwa pembacaan gagal.)

Disinkronkan

Antrian yang disinkronkan memiliki satu penulis dan satu pembaca dengan satu posisi tulis dan satu posisi baca. Tidak mungkin untuk menulis lebih banyak data daripada antrian yang memiliki ruang untuk atau membaca lebih banyak data daripada antrian saat ini. Tergantung pada apakah memblokir atau nonblocking menulis atau membaca fungsi dipanggil, upaya untuk melebihi ruang atau data yang tersedia baik mengembalikan kegagalan segera atau memblokir sampai operasi yang diinginkan dapat diselesaikan. Upaya untuk membaca atau menulis lebih banyak data daripada kapasitas antrian akan selalu segera gagal.

Menyiapkan FMQ

Antrian pesan memerlukan beberapa objek MessageQueue : satu untuk ditulis, dan satu atau lebih untuk dibaca. Tidak ada konfigurasi eksplisit objek mana yang digunakan untuk menulis atau membaca; terserah pengguna untuk memastikan bahwa tidak ada objek yang digunakan untuk membaca dan menulis, bahwa ada paling banyak satu penulis, dan, untuk antrian yang disinkronkan, ada paling banyak satu pembaca.

Membuat objek MessageQueue pertama

Antrian pesan dibuat dan dikonfigurasi dengan satu panggilan:

#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) membuat dan menginisialisasi objek yang mendukung fungsionalitas antrian pesan.
  • MessageQueue<T, flavor>(numElements, configureEventFlagWord) membuat dan menginisialisasi objek yang mendukung fungsionalitas antrian pesan dengan pemblokiran.
  • flavor dapat berupa kSynchronizedReadWrite untuk antrian yang disinkronkan atau kUnsynchronizedWrite untuk antrian yang tidak disinkronkan.
  • uint16_t (dalam contoh ini) dapat berupa tipe terdefinisi HIDL yang tidak melibatkan buffer bersarang (tanpa tipe string atau vec ), pegangan, atau antarmuka.
  • kNumElementsInQueue menunjukkan ukuran antrian dalam jumlah entri; itu menentukan ukuran buffer memori bersama yang akan dialokasikan untuk antrian.

Membuat objek MessageQueue kedua

Sisi kedua dari antrian pesan dibuat menggunakan objek MQDescriptor yang diperoleh dari sisi pertama. Objek MQDescriptor dikirim melalui panggilan HIDL RPC ke proses yang akan menahan ujung kedua antrian pesan. MQDescriptor berisi informasi tentang antrian, termasuk:

  • Informasi untuk memetakan buffer dan menulis pointer.
  • Informasi untuk memetakan penunjuk baca (jika antrian disinkronkan).
  • Informasi untuk memetakan kata bendera peristiwa (jika antrian menghalangi).
  • Jenis objek ( <T, flavor> ), yang mencakup jenis elemen antrian yang ditentukan HIDL dan ragam antrian (disinkronkan atau tidak disinkronkan).

Objek MQDescriptor dapat digunakan untuk membuat objek MessageQueue :

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

Parameter resetPointers menunjukkan apakah akan mereset posisi baca dan tulis ke 0 saat membuat objek MessageQueue ini. Dalam antrian yang tidak disinkronkan, posisi baca (yang bersifat lokal untuk setiap objek MessageQueue dalam antrian yang tidak disinkronkan) selalu disetel ke 0 selama pembuatan. Biasanya, MQDescriptor diinisialisasi selama pembuatan objek antrian pesan pertama. Untuk kontrol ekstra atas memori bersama, Anda dapat mengatur MQDescriptor secara manual ( MQDescriptor didefinisikan di system/libhidl/base/include/hidl/MQDescriptor.h ) lalu buat setiap objek MessageQueue seperti yang dijelaskan di bagian ini.

Memblokir antrian dan bendera acara

Secara default, antrian tidak mendukung pemblokiran baca/tulis. Ada dua jenis pemblokiran panggilan baca/tulis:

  • Bentuk singkat , dengan tiga parameter (penunjuk data, jumlah item, batas waktu). Mendukung pemblokiran pada operasi baca/tulis individu pada satu antrian. Saat menggunakan formulir ini, antrian akan menangani bendera peristiwa dan bitmask secara internal, dan objek antrian pesan pertama harus diinisialisasi dengan parameter kedua true . Misalnya:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Long form, with six parameters (includes event flag and bitmasks). Supports using a shared EventFlag object between multiple queues and allows specifying the notification bit masks to be used. In this case, the event flag and bitmasks must be supplied to each read and write call.

For the long form, the EventFlag can be supplied explicitly in each readBlocking() and writeBlocking() call. One of the queues may be initialized with an internal event flag, which must then be extracted from that queue's MessageQueue objects using getEventFlagWord() and used to create EventFlag objects in each process for use with other FMQs. Alternatively, the EventFlag objects can be initialized with any suitable shared memory.

In general, each queue should use only one of non-blocking, short-form blocking, or long-form blocking. It is not an error to mix them, but careful programming is required to get the desired result.

Using the MessageQueue

The public API of the MessageQueue object is:

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() dan availableToRead() dapat digunakan untuk menentukan berapa banyak data yang dapat ditransfer dalam satu operasi. Dalam antrian yang tidak disinkronkan:

  • availableToWrite() selalu mengembalikan kapasitas antrian.
  • Setiap pembaca memiliki posisi bacanya sendiri dan melakukan perhitungannya sendiri untuk availableToRead() .
  • Dari sudut pandang pembaca lambat, antrian dibiarkan meluap; ini dapat mengakibatkan availableToRead() mengembalikan nilai yang lebih besar dari ukuran antrian. Pembacaan pertama setelah overflow akan gagal dan mengakibatkan posisi baca untuk pembaca tersebut disetel sama dengan penunjuk tulis saat ini, terlepas dari apakah overflow dilaporkan melalui availableToRead() .

Metode read() dan write() mengembalikan nilai true jika semua data yang diminta dapat (dan telah) ditransfer ke/dari antrian. Metode ini tidak memblokir; mereka berhasil (dan mengembalikan true ), atau mengembalikan kegagalan ( false ) segera.

Metode readBlocking() dan writeBlocking() menunggu hingga operasi yang diminta dapat diselesaikan, atau hingga timeout (nilai timeOutNanos 0 berarti tidak pernah timeout).

Operasi pemblokiran diimplementasikan menggunakan kata bendera peristiwa. Secara default, setiap antrian membuat dan menggunakan kata benderanya sendiri untuk mendukung bentuk pendek readBlocking() dan writeBlocking() . Beberapa antrean dapat berbagi satu kata, sehingga suatu proses dapat menunggu untuk menulis atau membaca ke salah satu antrean. Pointer ke kata event flag antrian dapat diperoleh dengan memanggil getEventFlagWord() , dan pointer itu (atau pointer apa pun ke lokasi memori bersama yang sesuai) dapat digunakan untuk membuat objek EventFlag untuk diteruskan ke bentuk panjang readBlocking() dan writeBlocking() untuk antrian yang berbeda. Parameter readNotification dan writeNotification memberi tahu bit mana dalam flag event yang harus digunakan untuk memberi sinyal baca dan tulis pada antrian itu. readNotification dan writeNotification adalah bitmask 32-bit.

readBlocking() menunggu bit writeNotification ; jika parameter itu adalah 0, panggilan selalu gagal. Jika nilai readNotification adalah 0, panggilan tidak akan gagal, tetapi pembacaan yang berhasil tidak akan menetapkan bit notifikasi apa pun. Dalam antrian yang disinkronkan, ini berarti bahwa panggilan writeBlocking() yang sesuai tidak akan pernah aktif kecuali bit disetel di tempat lain. Dalam antrean yang tidak disinkronkan, writeBlocking() tidak akan menunggu (seharusnya masih digunakan untuk menyetel bit notifikasi tulis), dan sesuai untuk pembacaan agar tidak menyetel bit notifikasi apa pun. Demikian pula, writeblocking() akan gagal jika readNotification adalah 0, dan penulisan yang berhasil menyetel bit writeNotification yang ditentukan.

Untuk menunggu beberapa antrian sekaligus, gunakan metode wait() objek EventFlag untuk menunggu bitmask notifikasi. Metode wait() mengembalikan kata status dengan bit yang menyebabkan set bangun. Informasi ini kemudian digunakan untuk memverifikasi antrean yang sesuai memiliki cukup ruang atau data untuk operasi tulis/baca yang diinginkan dan lakukan nonblocking write() / read() . Untuk mendapatkan notifikasi pasca operasi, gunakan panggilan lain ke metode wake() EventFlag . Untuk definisi abstraksi EventFlag , lihat system/libfmq/include/fmq/EventFlag.h .

Operasi penyalinan nol

API read / write / readBlocking / writeBlocking() mengambil pointer ke buffer input/output sebagai argumen dan menggunakan panggilan memcpy() secara internal untuk menyalin data antara buffer cincin yang sama dan FMQ. Untuk meningkatkan kinerja, Android 8.0 dan yang lebih tinggi menyertakan serangkaian API yang menyediakan akses penunjuk langsung ke buffer dering, sehingga tidak perlu menggunakan panggilan memcpy .

Gunakan API publik berikut untuk operasi FMQ tanpa salinan:

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);
  • Metode beginWrite menyediakan pointer dasar ke buffer ring FMQ. Setelah data ditulis, komit menggunakan commitWrite() . Metode beginRead / commitRead bertindak dengan cara yang sama.
  • Metode beginRead / Write mengambil sebagai input jumlah pesan yang akan dibaca/ditulis dan mengembalikan boolean yang menunjukkan jika read/write memungkinkan. Jika membaca atau menulis dimungkinkan, struct memTx diisi dengan pointer dasar yang dapat digunakan untuk akses pointer langsung ke memori bersama buffer ring.
  • MemRegion berisi rincian tentang blok memori, termasuk penunjuk dasar (alamat dasar blok memori) dan panjang dalam hal T (panjang blok memori dalam hal tipe antrian pesan yang ditentukan oleh HIDL).
  • MemTransaction berisi dua struct MemRegion , first dan second sebagai membaca atau menulis ke buffer cincin mungkin memerlukan membungkus ke awal antrian. Ini berarti bahwa dua pointer dasar diperlukan untuk membaca/menulis data ke dalam buffer cincin FMQ.

Untuk mendapatkan alamat dan panjang dasar dari struct 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

Untuk mendapatkan referensi ke MemRegion s pertama dan kedua dalam objek MemTransaction :

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

Contoh menulis ke FMQ menggunakan nol copy API:

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
}

Metode pembantu berikut juga merupakan bagian dari MemTransaction :

  • T* getSlot(size_t idx);
    Mengembalikan pointer ke slot idx dalam MemRegions yang merupakan bagian dari objek MemTransaction ini. Jika objek MemTransaction mewakili wilayah memori untuk membaca/menulis N item bertipe T, maka rentang idx yang valid adalah antara 0 dan N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Tulis item nMessages tipe T ke dalam wilayah memori yang dijelaskan oleh objek, mulai dari indeks startIdx . Metode ini menggunakan memcpy() dan tidak dimaksudkan untuk digunakan untuk operasi penyalinan nol. Jika objek MemTransaction mewakili memori untuk membaca/menulis N item bertipe T, maka rentang idx yang valid adalah antara 0 dan N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Metode pembantu untuk membaca item nMessages tipe T dari wilayah memori yang dijelaskan oleh objek mulai dari startIdx . Metode ini menggunakan memcpy() dan tidak dimaksudkan untuk digunakan untuk operasi penyalinan nol.

Mengirim antrian melalui HIDL

Di sisi pembuatan:

  1. Buat objek antrian pesan seperti yang dijelaskan di atas.
  2. Verifikasi objek tersebut valid dengan isValid() .
  3. Jika Anda akan menunggu di beberapa antrian dengan meneruskan EventFlag ke dalam bentuk panjang readBlocking() / writeBlocking() , Anda dapat mengekstrak penunjuk flag peristiwa (menggunakan getEventFlagWord() ) dari objek MessageQueue yang diinisialisasi untuk membuat flag, dan gunakan flag itu untuk membuat objek EventFlag yang diperlukan.
  4. Gunakan metode MessageQueue getDesc() untuk mendapatkan objek deskriptor.
  5. Dalam file .hal , berikan metode parameter tipe fmq_sync atau fmq_unsync di mana T adalah tipe terdefinisi HIDL yang sesuai. Gunakan ini untuk mengirim objek yang dikembalikan oleh getDesc() ke proses penerima.

Di sisi penerima:

  1. Gunakan objek deskriptor untuk membuat objek MessageQueue . Pastikan untuk menggunakan ragam antrian dan tipe data yang sama, atau template akan gagal dikompilasi.
  2. Jika Anda mengekstrak bendera peristiwa, ekstrak bendera dari objek MessageQueue yang sesuai dalam proses penerimaan.
  3. Gunakan objek MessageQueue untuk mentransfer data.