Antrian Pesan Cepat (FMQ)

Jika Anda mencari dukungan AIDL, lihat juga FMQ dengan AIDL .

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. Objek MQDescriptorSync atau MQDescriptorUnsync dapat dikirim melalui panggilan HIDL RPC dan digunakan oleh proses penerimaan untuk mengakses antrian pesan.

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

Jenis Antrean Pesan

Android mendukung dua jenis antrean (dikenal sebagai varian ):

  • Antrean yang tidak tersinkronisasi 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 diperbolehkan mengalami underflow (pembacaan dari antrian kosong akan gagal) dan hanya dapat memiliki satu penulis.

Tidak disinkronkan

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

Penulisan ke antrean selalu berhasil (tidak dicentang untuk overflow) selama tidak lebih besar dari kapasitas antrean yang dikonfigurasi (penulisan yang 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 antrean setiap kali penulisan baru memerlukan ruang.

Pembaca bertanggung jawab untuk mengambil data sebelum data tersebut keluar dari antrian. Pembacaan yang mencoba membaca lebih banyak data daripada yang tersedia akan langsung gagal (jika non-pemblokiran) atau menunggu cukup data tersedia (jika memblokir). Pembacaan yang mencoba membaca lebih banyak data daripada kapasitas antrian selalu gagal.

Jika pembaca gagal mengikuti penulis, sehingga jumlah data yang ditulis dan belum dibaca oleh pembaca tersebut lebih besar dari kapasitas antrian, pembacaan berikutnya tidak mengembalikan data; sebaliknya, ia menyetel ulang posisi baca pembaca agar sama dengan posisi tulis terakhir, lalu mengembalikan kegagalan. Jika data tersedia untuk dibaca dicentang setelah overflow tetapi sebelum pembacaan berikutnya, ini menunjukkan lebih banyak data tersedia untuk dibaca daripada kapasitas antrian, yang menunjukkan telah terjadi overflow. (Jika antrean meluap antara pemeriksaan data yang tersedia dan upaya membaca data tersebut, satu-satunya indikasi luapan adalah pembacaan gagal.)

Pembaca antrean yang tidak disinkronkan kemungkinan besar tidak ingin menyetel ulang petunjuk baca dan tulis antrean. Jadi, saat membuat antrean dari deskriptor, pembaca harus menggunakan argumen `false` untuk parameter `resetPointers`.

Disinkronkan

Antrian tersinkronisasi memiliki satu penulis dan satu pembaca dengan satu posisi tulis dan satu posisi baca. Tidak mungkin untuk menulis lebih banyak data daripada ruang antrian atau membaca lebih banyak data daripada yang dimiliki antrian saat ini. Bergantung pada apakah fungsi tulis atau baca pemblokiran atau non-pemblokiran dipanggil, upaya untuk melebihi ruang atau data yang tersedia akan segera menghasilkan kegagalan atau memblokir hingga operasi yang diinginkan dapat diselesaikan. Upaya untuk membaca atau menulis lebih banyak data daripada kapasitas antrian akan selalu gagal.

Menyiapkan FMQ

Antrean pesan memerlukan beberapa objek MessageQueue : satu untuk menulis, dan satu atau lebih untuk membaca. Tidak ada konfigurasi eksplisit mengenai objek mana yang digunakan untuk menulis atau membaca; terserah pada pengguna untuk memastikan bahwa tidak ada objek yang digunakan untuk membaca dan menulis, paling banyak ada satu penulis, dan, untuk antrian tersinkronisasi, paling banyak ada 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 */);
  • Penginisialisasi 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 antrean tersinkronisasi atau kUnsynchronizedWrite untuk antrean yang tidak tersinkronisasi.
  • uint16_t (dalam contoh ini) dapat berupa tipe apa pun yang ditentukan HIDL yang tidak melibatkan buffer bersarang (tidak ada tipe string atau vec ), pegangan, atau antarmuka.
  • kNumElementsInQueue menunjukkan ukuran antrian dalam jumlah entri; ini 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 RPC HIDL atau AIDL 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 flag event (jika antrian menghalangi).
  • Tipe objek ( <T, flavor> ), yang mencakup tipe elemen antrean yang ditentukan HIDL dan ragam antrean (tersinkronisasi atau tidak tersinkronisasi).

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 diatur 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 dalam system/libhidl/base/include/hidl/MQDescriptor.h ) lalu membuat 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 pendek , dengan tiga parameter (penunjuk data, jumlah item, batas waktu). Mendukung pemblokiran operasi baca/tulis individual dalam satu antrian. Saat menggunakan formulir ini, antrian akan menangani event flag 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 */);
    
  • Bentuk panjang , dengan enam parameter (termasuk event flag dan bitmask). Mendukung penggunaan objek EventFlag bersama di antara beberapa antrian dan memungkinkan penentuan masker bit notifikasi yang akan digunakan. Dalam hal ini, event flag dan bitmask harus diberikan ke setiap panggilan baca dan tulis.

Untuk bentuk panjang, EventFlag dapat diberikan secara eksplisit di setiap panggilan readBlocking() dan writeBlocking() . Salah satu antrean dapat diinisialisasi dengan tanda peristiwa internal, yang kemudian harus diekstraksi dari objek MessageQueue antrean tersebut menggunakan getEventFlagWord() dan digunakan untuk membuat objek EventFlag di setiap proses untuk digunakan dengan FMQ lainnya. Alternatifnya, objek EventFlag dapat diinisialisasi dengan memori bersama apa pun yang sesuai.

Secara umum, setiap antrean hanya boleh menggunakan salah satu pemblokiran non-pemblokiran, pemblokiran bentuk pendek, atau pemblokiran bentuk panjang. Mencampurnya bukanlah suatu kesalahan, tetapi diperlukan pemrograman yang cermat untuk mendapatkan hasil yang diinginkan.

Menandai memori sebagai hanya baca

Secara default, memori bersama memiliki izin membaca dan menulis. Untuk antrian yang tidak disinkronkan ( kUnsynchronizedWrite ), penulis mungkin ingin menghapus izin menulis untuk semua pembaca sebelum membagikan objek MQDescriptorUnsync . Hal ini memastikan proses lain tidak dapat menulis ke antrian, yang direkomendasikan untuk melindungi terhadap bug atau perilaku buruk dalam proses pembaca. Jika penulis ingin pembaca dapat menyetel ulang antrean setiap kali mereka menggunakan MQDescriptorUnsync untuk membuat sisi baca antrean, maka memori tidak dapat ditandai sebagai hanya-baca. Ini adalah perilaku default konstruktor `MessageQueue`. Jadi, jika sudah ada pengguna antrian ini, kode mereka perlu diubah untuk membuat antrian dengan resetPointer=false .

  • Penulis: panggil ashmem_set_prot_region dengan deskriptor file MQDescriptor dan wilayah disetel ke read-only ( PROT_READ ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Pembaca: buat antrian pesan dengan resetPointer=false (defaultnya adalah true ):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Menggunakan MessageQueue

API publik dari objek MessageQueue adalah:

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 antrean yang tidak disinkronkan:

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

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

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

Operasi pemblokiran diimplementasikan menggunakan kata bendera acara. Secara default, setiap antrean membuat dan menggunakan kata benderanya sendiri untuk mendukung bentuk singkat readBlocking() dan writeBlocking() . Ada kemungkinan bagi beberapa antrian untuk berbagi satu kata, sehingga suatu proses dapat menunggu saat menulis atau membaca ke antrian mana pun. Sebuah penunjuk ke kata tanda peristiwa antrean dapat diperoleh dengan memanggil getEventFlagWord() , dan penunjuk tersebut (atau penunjuk apa pun ke lokasi memori bersama yang sesuai) dapat digunakan untuk membuat objek EventFlag untuk diteruskan ke dalam bentuk panjang readBlocking() dan writeBlocking() untuk antrian yang berbeda. Parameter readNotification dan writeNotification memberitahukan bit mana dalam flag event yang harus digunakan untuk memberi sinyal baca dan tulis pada antrian tersebut. readNotification dan writeNotification adalah bitmask 32-bit.

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

Untuk menunggu di beberapa antrean 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 antrian terkait memiliki cukup ruang atau data untuk operasi tulis/baca yang diinginkan dan melakukan write() / read() nonblocking. 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 penunjuk ke buffer input/output sebagai argumen dan menggunakan panggilan memcpy() secara internal untuk menyalin data antara buffer cincin FMQ dan yang sama. Untuk meningkatkan performa, Android 8.0 dan yang lebih tinggi menyertakan sekumpulan API yang menyediakan akses penunjuk langsung ke buffer ring, sehingga menghilangkan kebutuhan untuk menggunakan panggilan memcpy .

Gunakan API publik berikut untuk operasi FMQ tanpa penyalinan:

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 penunjuk dasar ke dalam buffer cincin FMQ. Setelah data ditulis, komit menggunakan commitWrite() . Metode beginRead / commitRead bertindak dengan cara yang sama.
  • Metode beginRead / Write mengambil input jumlah pesan yang akan dibaca/ditulis dan mengembalikan boolean yang menunjukkan apakah baca/tulis dapat dilakukan. Jika pembacaan atau penulisan dimungkinkan, struct memTx diisi dengan pointer dasar yang dapat digunakan untuk akses penunjuk langsung ke dalam memori bersama buffer ring.
  • Struct MemRegion berisi detail tentang blok memori, termasuk penunjuk dasar (alamat dasar blok memori) dan panjangnya dalam T (panjang blok memori dalam jenis antrian pesan yang ditentukan HIDL).
  • Struct MemTransaction berisi dua struct MemRegion , first dan second karena pembacaan atau penulisan ke dalam buffer ring mungkin memerlukan penyelesaian ke awal antrian. Ini berarti bahwa dua penunjuk dasar diperlukan untuk membaca/menulis data ke dalam buffer cincin FMQ.

Untuk mendapatkan alamat dasar dan panjang 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 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 zero 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 bertipe T ke dalam wilayah memori yang dijelaskan oleh objek, mulai dari indeks startIdx . Metode ini menggunakan memcpy() dan tidak dimaksudkan untuk digunakan pada 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 bertipe T dari wilayah memori yang dijelaskan oleh objek mulai dari startIdx . Metode ini menggunakan memcpy() dan tidak dimaksudkan untuk digunakan pada operasi penyalinan nol.

Mengirim antrian melalui HIDL

Di sisi pembuatan:

  1. Buat objek antrian pesan seperti dijelaskan di atas.
  2. Verifikasi objek valid dengan isValid() .
  3. Jika Anda akan menunggu di beberapa antrean dengan meneruskan EventFlag ke dalam bentuk panjang readBlocking() / writeBlocking() , Anda dapat mengekstrak penunjuk tanda peristiwa (menggunakan getEventFlagWord() ) dari objek MessageQueue yang diinisialisasi untuk membuat tanda, dan gunakan tanda 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 yang ditentukan HIDL yang sesuai. Gunakan ini untuk mengirim objek yang dikembalikan oleh getDesc() ke proses penerimaan.

Di pihak penerima:

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