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

HIDL'nin uzak prosedür çağrısı (RPC) altyapısı, bağlayıcı mekanizmaları kullanır. Yani çağrılar ek yük içerir, çekirdek işlemleri gerektirir ve planlayıcı işlemini tetikleyebilir. 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, alıcı işlem tarafından mesaj kuyruğuna erişmek için kullanılır.

Sıra türleri

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

  • Senkronize edilmemiş sıraların taşmasına izin verilir ve çok sayıda okuyucu olabilir. Her okuyucu, verileri zamanında okumalı veya kaybetmelidir.
  • 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. Bununla birlikte, 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.

Okuyucular, veriler sıranın sonuna düşmeden önce bunları almaktan sorumludur. 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 okuma işlemleri her zaman anında başarısız olur.

Bir okuyucu, yazara yetişemezse (bu nedenle, yazılan ve henüz okuyucu tarafından okunmayan veri miktarı, sıra kapasitesinden büyükse) sonraki okuma işlemi veri döndürmez. Bunun yerine, okuyucunun okuma konumunu en son yazma konumuna eşit olacak şekilde sıfırlar ve ardından hata döndürür. Okunabilir veriler, taşmadan sonra ancak sonraki okumadan önce kontrol edilirse okunabilir veri sayısı, sıra kapasitesinden fazla olur ve taşma meydana geldiğini gösterir. (Sıra, kullanılabilir verileri kontrol etme ve bu verileri okumaya çalışma arasında taşarsa taşmanın tek göstergesi okumanın başarısız olduğudur.)

Senkronize edilen sıralar

Senkronize bir kuyrukta tek bir yazma konumu ve tek bir okuma konumu olan bir yazar ve bir okuyucu bulunur. Sıranın şu anda sakladığından daha fazla veri yazmak veya 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ıranın 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 eşleme ve işaretçi yazma bilgileri.
  • 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).
  • Sıralı liste öğelerinin HIDL tarafından tanımlanan türünü ve sıralı liste çeşidini (senkronize veya senkronize edilmemiş) içeren nesne türü (<T, flavor>).

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 sırada, okuma konumu (senkronize edilmemiş sıralardaki her bir MessageQueue nesnesinin yerelidir) oluşturma sırasında her zaman 0'a 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şlemlerinin engellenmesini 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 biçim, 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ı parametreli (etkinlik işareti ve bit maskeleri dahil) uzun biçim, birden çok sıra arasında paylaşılan bir EventFlag nesnesinin kullanılmasını destekler ve kullanılacak bildirim bit maskelerinin belirtilmesini sağlar. Bu durumda, her okuma ve yazma çağrısına etkinlik işareti ve bit maskeleri sağlanmalıdır.

Uzun biçim için EventFlag öğesini her readBlocking() ve writeBlocking() çağrısında 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, diğer işlemlerin sıraya yazamamasını sağlar. Bu, okuyucu işlemlerindeki hatalara veya kötü davranışlara karşı koruma sağlamak için önerilir. Yazar, sıranın okunan tarafını oluşturmak için MQDescriptorUnsync kullandığında okuyucuların sırayı sıfırlayabilmesini isterse 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'yu 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. Taşma işleminden sonraki ilk okuma işlemi başarısız olur ve bu okuyucunun okuma konumu, availableToRead() aracılığıyla bildirilip bildirilmediğine bakılmaksızın mevcut yazma işaretçisine eşit şekilde ayarlanır.

read() ve write() yöntemleri, istenen tüm verilerin sıraya aktarılıp aktarılamadığına bağlı olarak true değerini 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 edilmiş bir sırada bu, bit başka bir yerde ayarlanmadığı sürece ilgili writeBlocking() çağrısının hiçbir zaman uyanmayacağı anlamına gelir. Senkronize edilmemiş bir sırada, writeBlocking() beklemez (yazma bildirimi bitini ayarlamak için yine de kullanılmalıdır) ve okumaların herhangi bir bildirim biti ayarlamaması uygundur. 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 bildirim bit maskesini beklemek üzere bir EventFlag nesnesininwait() 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. Bu bilgiler daha sonra ilgili sırada istenen yazma ve okuma işlemi için yeterli alana veya veriye sahip olduğunu doğrulamak ve engelleyici olmayan write() ve read() işlemleri 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 struct, bir bellek bloğuyla ilgili ayrıntıları içerir. Bu ayrıntılar arasında temel işaretçi (hafıza bloğunun temel adresi) ve T cinsinden uzunluk (HIDL tarafından tanımlanan mesaj sırasının HIDL tanımlı türü olarak bellek bloğunun uzunluğu) yer alır.
  • MemTransaction struct, iki MemRegion struct içerir; halka arabelleğine okuma veya yazma işlemi sırasında sıranın başlangıcına sarmalama gerekebilir. first ve second 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 okuyup yazacak bellek bölgelerini temsil ediyorsa geçerli idx aralığı 0 ile N-1 arasında olur.
  • 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. Açıklayıcı nesne almak için MessageQueuegetDesc() yöntemini 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 çıkardıysanız işareti alma işleminde karşılık gelen MessageQueue nesnesinden çıkarın.
  3. Veri aktarmak için MessageQueue nesnesini kullanın.