Hızlı Mesaj Sırası (FMQ)

HIDL'nin uzak prosedür çağrısı (RPC) altyapısı, bağlayıcı mekanizmaları kullanır. Bu, çağrıların ek maliyet içerdiği, çekirdek işlemlerini gerektirdiği ve planlayıcı işlemini tetikleyebileceği anlamına gelir. Ancak, verilerin daha az ek yük ve çekirdek katılımı olmadan işlemler arasında aktarılması gereken durumlarda Hızlı Mesaj Kuyruğu (FMQ) sistemi kullanılır.

FMQ, istenen özelliklere sahip mesaj sıraları oluşturur. HIDL RPC çağrısı üzerinden bir MQDescriptorSync veya MQDescriptorUnsync nesnesi gönderebilirsiniz. Bu nesne, mesaj kuyruğuna erişmek için alıcı işlem tarafından kullanılır.

Sıra türleri

Android iki sıra türünü (lezzetler olarak bilinir) destekler:

  • Senkronize edilmemiş sıraların taşmasına izin verilir ve bu sıralarda çok sayıda okuyucu olabilir. Her okuyucu, verileri zamanında okumalı veya kaybeder.
  • Senkronize sıraların taşmasına izin verilmez ve bu sıralarda yalnızca bir okuyucu olabilir.

Her iki sıra türünde de alt akışa izin verilmez (boş bir sıradan okuma başarısız olur) ve yalnızca bir yazar olabilir.

Senkronize edilmemiş sıralar

Senkronize edilmemiş bir kuyrukta yalnızca bir yazar ancak herhangi bir sayıda okuyucu olabilir. Sıra için bir yazma konumu vardır ancak her okuyucu kendi bağımsız okuma konumunu izler.

Yapılandırılmış sıra kapasitesinden büyük olmadıkları sürece sıraya yapılan yazma işlemleri her zaman başarılı olur (taşma olup olmadığı kontrol edilmez) (sıra kapasitesinden büyük yazma işlemleri hemen başarısız olur). Her okuyucunun farklı bir okuma konumu olabileceğinden, her okuyucunun her veri parçasını okumasını beklemek yerine, yeni yazma işlemlerinin alana ihtiyacı olduğunda veriler kuyruktan çıkarılır.

Verileri, kuyruğun sonuna düşmeden önce almak okuyucuların sorumluluğundadır. Mevcut olandan daha fazla veri okumaya çalışan bir okuma işlemi, engellenmeyen durumlarda hemen başarısız olur veya engelleyen durumlarda yeterli verinin mevcut olmasını bekler. Sıra kapasitesinden daha fazla veri okumaya çalışan bir okuma işlemi her zaman hemen başarısız olur.

Bir okuyucu, yazara yetişemezse (ör. yazılan ve henüz bu okuyucu tarafından okunmayan veri miktarı, sıra kapasitesini aşarsa) bir sonraki okuma işlemi veri döndürmez. Bunun yerine, okuyucunun okuma konumunu yazma konumuna ve kapasitenin yarısına sıfırlar ve ardından bir hata döndürür. Bu sayede, arabelleğin yarısı okuma için kullanılabilir durumda kalır ve kuyruğun hemen tekrar dolmasını önlemek için yeni yazma işlemleri için yer ayrılır. Okunabilir veriler, taşmadan sonra ancak bir sonraki okumadan önce kontrol edilirse okunabilir veri sayısı, sıra kapasitesinden daha fazla olur. Bu da taşma olduğunu gösterir. (Kuyruk, mevcut verileri kontrol etme ve bu verileri okuma denemesi arasında taşarsa taşmanın tek göstergesi okuma işleminin başarısız olmasıdır.)

Senkronize edilen sıraları

Senkronize bir kuyrukta tek bir yazma ve tek bir okuma konumu olan bir yazar ve bir okuyucu bulunur. Sırada yer alan alan miktarından daha fazla veri yazmak veya sırada bulunandan daha fazla veri okumak mümkün değildir. Engelleyici veya engellemeyen yazma ya da okuma işlevinin çağrılıp çağrılmadığına bağlı olarak, mevcut alanı veya verileri aşma girişimleri hemen başarısız olur ya da istenen işlem tamamlanana kadar engellenir. Sıra kapasitesinden daha fazla veri okuma veya yazma girişimleri her zaman hemen başarısız olur.

FMQ oluşturma

Mesaj kuyruğu için birden fazla MessageQueue nesnesi gerekir: Biri yazılacak, biri veya daha fazlası okunacak. Yazma veya okuma için hangi nesnenin kullanılacağına dair açık bir yapılandırma yoktur. Kullanıcı, hem okuma hem de yazma için hiçbir nesnenin kullanılmadığından, en fazla bir yazar bulunduğundan ve senkronize edilen sıralarda en fazla bir okuyucu bulunduğundan emin olmalıdır.

İlk MessageQueue nesnesini oluşturma

Tek bir çağrıyla bir mesaj kuyruğu oluşturulur ve yapılandırılır:

#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 nonblocking 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) başlatıcısı, mesaj kuyruğu işlevini destekleyen bir nesne oluşturur ve başlatır.
  • MessageQueue<T, flavor>(numElements, configureEventFlagWord) başlatıcısı, mesaj kuyruğu işlevini engellemeyle destekleyen bir nesne oluşturup başlatır.
  • flavor, senkronize edilmiş bir sıra için kSynchronizedReadWrite veya senkronize edilmemiş bir sıra için kUnsynchronizedWrite olabilir.
  • uint16_t (bu örnekte), iç içe yerleştirilmiş arabellekleri (string veya vec türleri yok), imleçleri veya arayüzleri içermeyen herhangi bir HIDL tanımlı tür olabilir.
  • kNumElementsInQueue, giriş sayısı olarak kuyruğun boyutunu belirtir; kuyruk için ayrılan paylaşılan bellek arabelleğinin boyutunu belirler.

İkinci MessageQueue nesnesini oluşturun

Mesaj kuyruğunun ikinci tarafı, birinci taraftan elde edilen bir MQDescriptor nesnesi kullanılarak oluşturulur. MQDescriptor nesnesi, mesaj kuyruğunun ikinci ucunu tutan işleme HIDL veya AIDL RPC çağrısı üzerinden gönderilir. MQDescriptor, aşağıdakiler dahil olmak üzere sırayla ilgili bilgileri içerir:

  • Arabelleği ve yazma işaretçisini eşlemek için bilgiler.
  • Okuma işaretçisini eşlemek için gereken bilgiler (sıra senkronize edilmişse).
  • Etkinlik işareti kelimesini eşlemek için gereken bilgiler (sıra engelliyorsa).
  • Nesne türü (<T, flavor>), sıra öğelerinin HIDL tarafından tanımlanan türünü ve sıra çeşidini (senkronize veya senkronize edilmemiş) içerir.

MessageQueue nesnesi oluşturmak için MQDescriptor nesnesini kullanabilirsiniz:

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

resetPointers parametresi, bu MessageQueue nesnesi oluşturulurken okuma ve yazma konumlarının 0'a sıfırlanıp sıfırlanmayacağını belirtir. Senkronize edilmemiş bir kuyrukta, okuma konumu (senkronize edilmemiş kuyruklardaki her MessageQueue nesnesine yereldir) oluşturulma sırasında her zaman 0 olarak ayarlanır. MQDescriptor genellikle ilk mesaj kuyruğu nesnesi oluşturulurken başlatılır. Paylaşılan bellek üzerinde daha fazla kontrol sahibi olmak için MQDescriptor'ü manuel olarak ayarlayabilir (MQDescriptor, system/libhidl/base/include/hidl/MQDescriptor.h içinde tanımlanır) ve ardından her MessageQueue nesnesini bu bölümde açıklandığı gibi oluşturabilirsiniz.

Engelleme sıraları ve etkinlik işaretleri

Varsayılan olarak, sıralar okuma ve yazma işlemlerini engellemeyi desteklemez. Okuma ve yazma çağrılarını engellemenin iki yolu vardır:

  • Üç parametre (veri işaretçisi, öğe sayısı, zaman aşımı) içeren kısa form, tek bir kuyrukta tek tek okuma ve yazma işlemlerinde engellemeyi destekler. Bu form kullanıldığında, sıra etkinlik işaretini ve bit maskelerini dahili olarak işler ve ilk mesaj sırası nesnesi, true değerine sahip ikinci bir parametreyle başlatılmalıdır. Örnek:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Altı parametre (etkinlik işareti ve bit maskeleri dahil) içeren uzun biçim, birden fazla sıra arasında paylaşılan bir EventFlag nesnesi kullanılmasını destekler ve kullanılacak bildirim bit maskelerini belirtmenize olanak tanır. Bu durumda, her okuma ve yazma çağrısına etkinlik işareti ve bit maskeleri sağlanmalıdır.

Uzun form için her readBlocking() ve writeBlocking() çağrısında EventFlag değerini açıkça sağlayabilirsiniz. Kuyruklardan birini dahili bir etkinlik işaretiyle başlatabilirsiniz. Bu işaret daha sonra getEventFlagWord() kullanılarak ilgili kuyruğun MessageQueue nesnelerinden ayıklanır ve diğer FMQ'lerle birlikte kullanılmak üzere her işlemde bir EventFlag nesnesi oluşturmak için kullanılır. Alternatif olarak, EventFlag nesnelerini uygun herhangi bir paylaşılan bellekle başlatabilirsiniz.

Genel olarak her sırada yalnızca engelleme, kısa video engelleme veya uzun video engelleme seçeneklerinden biri kullanılmalıdır. Bunları karıştırmak hata değildir ancak istenen sonucu elde etmek için dikkatli bir şekilde programlama yapmanız gerekir.

Belleği salt okunur olarak işaretleme

Paylaşılan bellek varsayılan olarak okuma ve yazma izinlerine sahiptir. Senkronize edilmemiş sıralarda (kUnsynchronizedWrite), yazar MQDescriptorUnsync nesnelerini dağıtmadan önce tüm okuyucuların yazma izinlerini kaldırmak isteyebilir. Bu sayede diğer işlemlerin sıraya yazması engellenir. Bu, okuyucu işlemlerindeki hatalara veya kötü davranışlara karşı koruma sağlamak için önerilir. Yazıcı, okuyucuların kuyruğun okuma tarafını oluşturmak için MQDescriptorUnsync kullandıklarında kuyruğu sıfırlayabilmesini istiyorsa bellek salt okunur olarak işaretlenemez. Bu, MessageQueue kurucusunun varsayılan davranışıdır. Bu nedenle, bu kuyruğun mevcut kullanıcıları varsa kuyruğu resetPointer=false ile oluşturacak şekilde kodlarının değiştirilmesi gerekir.

  • Yazıcı: MQDescriptor dosya tanımlayıcısıyla ve bölge salt okunur olarak ayarlanmış (PROT_READ) ashmem_set_prot_region çağrısı yapın:
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Okuyucu: resetPointer=false ile mesaj kuyruğu oluşturun (varsayılan true'tür):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

MessageQueue'ı kullanma

MessageQueue nesnesinin herkese açık API'si:

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

Tek bir işlemde ne kadar veri aktarılabileceğini belirlemek için availableToWrite() ve availableToRead() öğelerini kullanabilirsiniz. Senkronize edilmemiş bir sırada:

  • availableToWrite() her zaman kuyruğun kapasitesini döndürür.
  • Her okuyucunun kendi okuma konumu vardır ve availableToRead() için kendi hesaplamasını yapar.
  • Yavaş bir okuyucunun bakış açısından, kuyruğun taşmasına izin verilir. Bu durum, availableToRead()'ün kuyruğun boyutundan daha büyük bir değer döndürmesine neden olabilir. Aşırı taşma sonrasında yapılan ilk okuma başarısız olur ve taşmanın availableToRead() aracılığıyla bildirilip bildirilmediğine bakılmaksızın, söz konusu okuyucunun okuma konumunun geçerli yazma işaretçisine eşit ayarlanmasına neden olur.

read() ve write() yöntemleri, istenen tüm verilerin sıraya aktarılıp aktarılamadığını (ve aktarılıp aktarılmadığını) true döndürür. Bu yöntemler engelleme yapmaz; ya başarılı olur (ve true döndürür) ya da hemen başarısız olur (false döndürür).

readBlocking() ve writeBlocking() yöntemleri, istenen işlemin tamamlanmasını veya zaman aşımına uğramasını bekler (timeOutNanos değeri 0 ise zaman aşımı asla gerçekleşmez).

Engelleme işlemleri, etkinlik işareti sözcüğü kullanılarak uygulanır. Varsayılan olarak her sıra, readBlocking() ve writeBlocking() kısa biçimlerini desteklemek için kendi işaret kelimesini oluşturur ve kullanır. Birden fazla sıra tek bir kelimeyi paylaşabilir. Böylece bir işlem, sıralardan herhangi birine yazma veya okuma işlemini bekleyebilir. getEventFlagWord() işlevini çağırarak bir kuyruğun etkinlik işareti sözcüğüne işaretçi alabilir ve bu işaretçiyi (veya uygun bir paylaşılan bellek konumuna işaret eden herhangi bir işaretçiyi) farklı bir kuyruk için readBlocking() ve writeBlocking() işlevinin uzun biçimine iletilecek bir EventFlag nesnesi oluşturmak üzere kullanabilirsiniz. readNotification ve writeNotification parametreleri, etkinlik işaretindeki hangi bitlerin söz konusu sıradaki okuma ve yazma işlemlerini işaretlemek için kullanılacağını belirtir. readNotification ve writeNotification, 32 bitlik bit maskeleridir.

readBlocking(), writeNotification bitlerini bekler; bu parametre 0 ise çağrı her zaman başarısız olur. readNotification değeri 0 ise çağrı başarısız olmaz ancak başarılı bir okuma, bildirim bitlerini ayarlamaz. Senkronize bir kuyrukta bu, bit başka bir yerde ayarlanmadıkça ilgili writeBlocking() çağrısının hiçbir zaman uyanmamasıdır. Senkronize edilmemiş bir kuyrukta writeBlocking() beklemez (yine de yazma bildirimi biti ayarlamak için kullanılmalıdır) ve okumaların bildirim biti ayarlamamasının uygun olduğu kabul edilir. Benzer şekilde, readNotification 0 ise writeblocking() başarısız olur ve başarılı bir yazma işlemi, belirtilen writeNotification bitlerini ayarlar.

Aynı anda birden fazla sırada beklemek için bildirimlerin bit maskesini beklemek üzere bir EventFlag nesnesinin wait() yöntemini kullanın. wait() yöntemi, uyanma ayarının yapılmasına neden olan bitleri içeren bir durum kelimesi döndürür. Ardından bu bilgiler, ilgili kuyruğun istenen yazma ve okuma işlemi için yeterli alana veya veriye sahip olduğunu doğrulamak ve engellenmeyen bir write() ve read() gerçekleştirmek için kullanılır. İşlem sonrası bildirim almak için EventFlag nesnesinin wake() yöntemine başka bir çağrı yapın. EventFlag Abstreksiyonun tanımı için system/libfmq/include/fmq/EventFlag.h bölümüne bakın.

Kopyasız işlemler

read, write, readBlocking ve writeBlocking() yöntemleri, bağımsız değişken olarak bir giriş/çıkış arabelleğinin işaretçisini alır ve aynı arabellek ile FMQ halka arabelleği arasında veri kopyalamak için dahili olarak memcpy() çağrılarını kullanır. Android 8.0 ve sonraki sürümler, performansı artırmak için halka arabelleğe doğrudan işaretçi erişimi sağlayan bir API grubu içerir. Bu sayede memcpy çağrılarını kullanma ihtiyacı ortadan kalkar.

Kopyasız FMQ işlemleri için aşağıdaki herkese açık API'leri kullanın:

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 yöntemi, FMQ halka arabelleğine temel işaretçiler sağlar. Veriler yazıldıktan sonra commitWrite() kullanarak verileri gönderin. beginRead ve commitRead yöntemleri aynı şekilde çalışır.
  • beginRead ve Write yöntemleri, okunacak ve yazılacak mesajların sayısını giriş olarak alır ve okuma ya da yazma işleminin mümkün olup olmadığını belirten bir Boole değeri döndürür. Okuma veya yazma mümkünse memTx struct, halka arabelleğinin paylaşılan belleğine doğrudan işaretçi erişimi için kullanılabilecek temel işaretçilerle doldurulur.
  • MemRegion yapısı, taban işaretçi (bellek bloğunun taban adresi) ve T açısından uzunluk (mesaj kuyruğunun HIDL tarafından tanımlanan türü açısından bellek bloğunun uzunluğu) dahil olmak üzere bir bellek bloğuyla ilgili ayrıntıları içerir.
  • MemTransaction yapı, halka tamponuna okuma veya yazma işleminin, kuyruğun başına dönmeyi gerektirebileceği için iki MemRegion yapısı (first ve second) içerir. Bu, FMQ halka arabelleğine veri okumak ve yazmak için iki temel işaretçi gerektiği anlamına gelir.

Bir MemRegion yapısından taban adresi ve uzunluğu almak için:

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

Bir MemTransaction nesnesinde ilk ve ikinci MemRegion yapılarına referans almak için:

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

Sıfır kopyalama API'lerini kullanarak FMQ'ye yazma örneği:

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
}

Aşağıdaki yardımcı yöntemler de MemTransaction'e dahildir:

  • T* getSlot(size_t idx);, bu MemTransaction nesnesinin parçası olan MemRegions içindeki idx yuvasına işaret eden bir işaretçi döndürür. MemTransaction nesnesi, T türündeki N öğeyi okumak ve yazmak için bellek bölgelerini temsil ediyorsa idx için geçerli aralık 0 ile N-1 arasındadır.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);, startIdx dizininden başlayarak T türündeki nMessages öğeleri nesne tarafından açıklanan bellek bölgelerine yazar. Bu yöntem memcpy() kullanır ve sıfır kopyalama işlemi için kullanılmak üzere tasarlanmamıştır. MemTransaction nesnesi, T türündeki N öğeyi okumak ve yazmak için belleği temsil ediyorsa idx için geçerli aralık 0 ile N-1 arasındadır.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);, startIdx'den başlayarak nesne tarafından açıklanan bellek bölgelerinden T türündeki nMessages öğeyi okumak için kullanılan bir yardımcı yöntemdir. Bu yöntem memcpy() kullanır ve sıfır kopyalama işlemi için kullanılmak üzere tasarlanmamıştır.

Sırayı HIDL üzerinden gönderme

İçerik üreten taraf için:

  1. Yukarıda açıklandığı şekilde bir ileti kuyruğu nesnesi oluşturun.
  2. Nesnenin isValid() ile geçerli olduğunu doğrulayın.
  3. EventFlag öğesini readBlocking() veya writeBlocking() öğesinin uzun biçimine göndererek birden fazla sırada bekliyorsanız işaretçi oluşturmak için başlatılmış bir MessageQueue nesnesinden etkinlik işaretçisini (getEventFlagWord() kullanarak) ayıklayabilir ve gerekli EventFlag nesnesini oluşturmak için bu işaretçiyi kullanabilirsiniz.
  4. Tanımlayıcı nesnesi almak için MessageQueue yöntemini getDesc() kullanın.
  5. HAL dosyasında, yönteme fmq_sync veya fmq_unsync türündeki bir parametre verin. Burada T, uygun bir HIDL tanımlı türdür. getDesc() tarafından döndürülen nesneyi alıcı işleme göndermek için bunu kullanın.

Alıcı tarafında:

  1. MessageQueue nesnesi oluşturmak için tanımlayıcı nesnesini kullanın. Aynı sıra çeşidini ve veri türünü kullanın. Aksi takdirde şablon derlenemez.
  2. Bir etkinlik işareti ayıkladıysanız işareti, alıcı işlemdeki ilgili MessageQueue nesnesinden ayıklayın.
  3. Veri aktarmak için MessageQueue nesnesini kullanın.