Coda messaggi veloce (FMQ)

Se stai cercando il supporto AIDL, vedi anche FMQ con AIDL .

L'infrastruttura RPC (Remote Procedure Call) di HIDL utilizza meccanismi Binder, il che significa che le chiamate comportano un sovraccarico, richiedono operazioni del kernel e possono attivare un'azione di pianificazione. Tuttavia, per i casi in cui i dati devono essere trasferiti tra processi con meno sovraccarico e senza coinvolgimento del kernel, viene utilizzato il sistema Fast Message Queue (FMQ).

FMQ crea code di messaggi con le proprietà desiderate. Un oggetto MQDescriptorSync o MQDescriptorUnsync può essere inviato tramite una chiamata RPC HIDL e utilizzato dal processo ricevente per accedere alla coda dei messaggi.

Le code di messaggi veloci sono supportate solo in C++ e nei dispositivi che eseguono Android 8,0 e versioni successive.

Tipi MessageQueue

Android supporta due tipi di coda (noti come sapori ):

  • Le code non sincronizzate possono traboccare e possono avere molti lettori; ogni lettore deve leggere i dati in tempo altrimenti li perderà.
  • Le code sincronizzate non possono traboccare e possono avere un solo lettore.

Entrambi i tipi di coda non possono eseguire un underflow (la lettura da una coda vuota fallirà) e possono avere un solo scrittore.

Non sincronizzato

Una coda non sincronizzata ha un solo scrittore, ma può avere un numero qualsiasi di lettori. Esiste una posizione di scrittura per la coda; tuttavia ciascun lettore tiene traccia della propria posizione di lettura indipendente.

Le scritture sulla coda hanno sempre esito positivo (non vengono controllate per l'overflow) purché non siano più grandi della capacità della coda configurata (le scritture più grandi della capacità della coda falliscono immediatamente). Poiché ogni lettore può avere una posizione di lettura diversa, invece di aspettare che ogni lettore legga ogni dato, i dati possono cadere dalla coda ogni volta che nuove scritture richiedono spazio.

I lettori sono responsabili del recupero dei dati prima che cadano alla fine della coda. Una lettura che tenta di leggere più dati di quelli disponibili fallisce immediatamente (se non blocca) o attende che siano disponibili dati sufficienti (se blocca). Una lettura che tenta di leggere più dati rispetto alla capacità della coda fallisce sempre immediatamente.

Se un lettore non riesce a tenere il passo con lo scrittore, tanto che la quantità di dati scritti e non ancora letti da quel lettore è maggiore della capacità della coda, la lettura successiva non restituisce dati; invece, reimposta la posizione di lettura del lettore in modo che corrisponda all'ultima posizione di scrittura, quindi restituisce un errore. Se i dati disponibili per la lettura vengono controllati dopo l'overflow ma prima della lettura successiva, vengono visualizzati più dati disponibili per la lettura rispetto alla capacità della coda, indicando che si è verificato un overflow. (Se la coda va in overflow tra il controllo dei dati disponibili e il tentativo di leggere tali dati, l'unica indicazione di overflow è che la lettura non riesce.)

I lettori di una coda non sincronizzata probabilmente non vorranno reimpostare i puntatori di lettura e scrittura della coda. Pertanto, quando si crea la coda dal descrittore, i lettori dovrebbero utilizzare un argomento "false" per il parametro "resetPointers".

Sincronizzato

Una coda sincronizzata ha uno scrittore e un lettore con un'unica posizione di scrittura e un'unica posizione di lettura. È impossibile scrivere più dati di quelli di cui dispone la coda o leggere più dati di quelli attualmente contenuti nella coda. A seconda che venga chiamata la funzione di scrittura o lettura bloccante o non bloccante, i tentativi di superare lo spazio disponibile o i dati restituiscono un errore immediatamente o si bloccano fino al completamento dell'operazione desiderata. I tentativi di leggere o scrivere più dati rispetto alla capacità della coda falliranno sempre immediatamente.

Impostazione di una FMQ

Una coda di messaggi richiede più oggetti MessageQueue : uno su cui scrivere e uno o più da cui leggere. Non esiste una configurazione esplicita di quale oggetto venga utilizzato per la scrittura o la lettura; spetta all'utente assicurarsi che nessun oggetto venga utilizzato sia per la lettura che per la scrittura, che ci sia al più uno scrittore e, per le code sincronizzate, che ci sia al più un lettore.

Creazione del primo oggetto MessageQueue

Una coda di messaggi viene creata e configurata con una singola chiamata:

#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 */);
  • L'inizializzatore MessageQueue<T, flavor>(numElements) crea e inizializza un oggetto che supporta la funzionalità della coda di messaggi.
  • L' MessageQueue<T, flavor>(numElements, configureEventFlagWord) crea e inizializza un oggetto che supporta la funzionalità della coda di messaggi con il blocco.
  • flavor può essere kSynchronizedReadWrite per una coda sincronizzata o kUnsynchronizedWrite per una coda non sincronizzata.
  • uint16_t (in questo esempio) può essere qualsiasi tipo definito da HIDL che non coinvolge buffer nidificati (nessun tipo string o vec ), handle o interfacce.
  • kNumElementsInQueue indica la dimensione della coda in numero di voci; determina la dimensione del buffer di memoria condivisa che verrà allocato per la coda.

Creazione del secondo oggetto MessageQueue

Il secondo lato della coda di messaggi viene creato utilizzando un oggetto MQDescriptor ottenuto dal primo lato. L'oggetto MQDescriptor viene inviato tramite una chiamata RPC HIDL o AIDL al processo che manterrà la seconda estremità della coda di messaggi. Il MQDescriptor contiene informazioni sulla coda, tra cui:

  • Informazioni per mappare il buffer e scrivere il puntatore.
  • Informazioni per mappare il puntatore di lettura (se la coda è sincronizzata).
  • Informazioni per mappare la parola del flag evento (se la coda è bloccata).
  • Tipo di oggetto ( <T, flavor> ), che include il tipo di elementi della coda definiti da HIDL e il tipo di coda (sincronizzato o non sincronizzato).

L'oggetto MQDescriptor può essere utilizzato per costruire un oggetto MessageQueue :

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

Il parametro resetPointers indica se reimpostare le posizioni di lettura e scrittura su 0 durante la creazione di questo oggetto MessageQueue . In una coda non sincronizzata, la posizione di lettura (che è locale per ciascun oggetto MessageQueue nelle code non sincronizzate) è sempre impostata su 0 durante la creazione. In genere, MQDescriptor viene inizializzato durante la creazione del primo oggetto coda messaggi. Per un controllo extra sulla memoria condivisa, è possibile impostare manualmente MQDescriptor ( MQDescriptor è definito in system/libhidl/base/include/hidl/MQDescriptor.h ) quindi creare ogni oggetto MessageQueue come descritto in questa sezione.

Blocco delle code e dei flag di evento

Per impostazione predefinita, le code non supportano il blocco delle letture/scritture. Esistono due tipi di blocco delle chiamate di lettura/scrittura:

  • Forma breve , con tre parametri (puntatore dati, numero di elementi, timeout). Supporta il blocco di singole operazioni di lettura/scrittura su una singola coda. Quando si utilizza questo modulo, la coda gestirà internamente il flag evento e le maschere di bit e il primo oggetto della coda messaggi deve essere inizializzato con un secondo parametro true . Ad esempio:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Forma lunga , con sei parametri (include flag di evento e maschere di bit). Supporta l'utilizzo di un oggetto EventFlag condiviso tra più code e consente di specificare le maschere di bit di notifica da utilizzare. In questo caso, il flag evento e le maschere di bit devono essere forniti a ciascuna chiamata di lettura e scrittura.

Per la forma lunga, EventFlag può essere fornito esplicitamente in ciascuna chiamata readBlocking() e writeBlocking() . Una delle code può essere inizializzata con un flag di evento interno, che deve quindi essere estratto dagli oggetti MessageQueue di quella coda utilizzando getEventFlagWord() e utilizzato per creare oggetti EventFlag in ciascun processo da utilizzare con altri FMQ. In alternativa, gli oggetti EventFlag possono essere inizializzati con una qualsiasi memoria condivisa adatta.

In generale, ciascuna coda dovrebbe utilizzare solo una delle opzioni non bloccante, blocco in formato breve o blocco in formato lungo. Non è un errore mescolarli, ma è necessaria un'attenta programmazione per ottenere il risultato desiderato.

Contrassegnare la memoria come di sola lettura

Per impostazione predefinita, la memoria condivisa dispone di autorizzazioni di lettura e scrittura. Per le code non sincronizzate ( kUnsynchronizedWrite ), lo scrittore potrebbe voler rimuovere i permessi di scrittura per tutti i lettori prima di distribuire gli oggetti MQDescriptorUnsync . Ciò garantisce che gli altri processi non possano scrivere nella coda, il che è consigliato per proteggersi da bug o comportamenti scorretti nei processi di lettura. Se lo scrittore desidera che i lettori siano in grado di reimpostare la coda ogni volta che utilizzano MQDescriptorUnsync per creare il lato di lettura della coda, la memoria non può essere contrassegnata come di sola lettura. Questo è il comportamento predefinito del costruttore "MessageQueue". Pertanto, se esistono già utenti di questa coda, il loro codice deve essere modificato per costruire la coda con resetPointer=false .

  • Scrittore: chiama ashmem_set_prot_region con un descrittore di file MQDescriptor e una regione impostata su sola lettura ( PROT_READ ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Lettore: crea una coda di messaggi con resetPointer=false (il valore predefinito è true ):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Utilizzando MessageQueue

L'API pubblica dell'oggetto MessageQueue è:

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() e availableToRead() possono essere utilizzati per determinare la quantità di dati che possono essere trasferiti in una singola operazione. In una coda non sincronizzata:

  • availableToWrite() restituisce sempre la capacità della coda.
  • Ogni lettore ha la propria posizione di lettura ed esegue i propri calcoli per availableToRead() .
  • Dal punto di vista di un lettore lento, la coda può traboccare; ciò potrebbe far sì che availableToRead() restituisca un valore maggiore della dimensione della coda. La prima lettura dopo un overflow fallirà e comporterà che la posizione di lettura per quel lettore venga impostata uguale al puntatore di scrittura corrente, indipendentemente dal fatto che l'overflow sia stato segnalato o meno tramite availableToRead() .

I metodi read() e write() restituiscono true se tutti i dati richiesti potevano essere (e sono stati) trasferiti alla/dalla coda. Questi metodi non bloccano; hanno successo (e restituiscono true ) o restituiscono immediatamente un fallimento ( false ).

I metodi readBlocking() e writeBlocking() attendono fino al completamento dell'operazione richiesta o fino al timeout (un valore timeOutNanos pari a 0 significa mai timeout).

Le operazioni di blocco vengono implementate utilizzando una parola di flag di evento. Per impostazione predefinita, ogni coda crea e utilizza la propria parola flag per supportare la forma breve di readBlocking() e writeBlocking() . È possibile che più code condividano una singola parola, in modo che un processo possa attendere la scrittura o la lettura di una qualsiasi delle code. Un puntatore alla parola del flag di evento di una coda può essere ottenuto chiamando getEventFlagWord() e quel puntatore (o qualsiasi puntatore a una posizione di memoria condivisa adatta) può essere utilizzato per creare un oggetto EventFlag da passare nella forma lunga di readBlocking() e writeBlocking() per una coda diversa. I parametri readNotification e writeNotification indicano quali bit nel flag di evento devono essere utilizzati per segnalare letture e scritture su quella coda. readNotification e writeNotification sono maschere di bit a 32 bit.

readBlocking() attende i bit writeNotification ; se il parametro è 0, la chiamata fallisce sempre. Se il valore readNotification è 0, la chiamata non fallirà, ma una lettura riuscita non imposterà alcun bit di notifica. In una coda sincronizzata, ciò significherebbe che la corrispondente chiamata writeBlocking() non si attiverà mai a meno che il bit non sia impostato altrove. In una coda non sincronizzata, writeBlocking() non attenderà (dovrebbe comunque essere utilizzato per impostare il bit di notifica di scrittura) ed è opportuno che le letture non impostino alcun bit di notifica. Allo stesso modo, writeblocking() fallirà se readNotification è 0 e una scrittura riuscita imposta i bit writeNotification specificati.

Per attendere su più code contemporaneamente, utilizzare il metodo wait() di un oggetto EventFlag per attendere una maschera di bit di notifiche. Il metodo wait() restituisce una parola di stato con i bit che hanno causato l'impostazione della riattivazione. Queste informazioni vengono quindi utilizzate per verificare che la coda corrispondente disponga di spazio o dati sufficienti per l'operazione di scrittura/lettura desiderata ed eseguire un'operazione write() / read() non bloccante. Per ottenere una notifica post-operazione, utilizzare un'altra chiamata al metodo wake() di EventFlag . Per una definizione dell'astrazione EventFlag , fare riferimento a system/libfmq/include/fmq/EventFlag.h .

Zero operazioni di copia

Le API read / write / readBlocking / writeBlocking() accettano un puntatore a un buffer di input/output come argomento e utilizzano le chiamate memcpy() internamente per copiare i dati tra lo stesso e il buffer dell'anello FMQ. Per migliorare le prestazioni, Android 8.0 e versioni successive includono un set di API che forniscono accesso diretto al puntatore nel ring buffer, eliminando la necessità di utilizzare chiamate memcpy .

Utilizza le seguenti API pubbliche per operazioni FMQ a copia zero:

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);
  • Il metodo beginWrite fornisce puntatori di base nel buffer dell'anello FMQ. Dopo che i dati sono stati scritti, esegui il commit utilizzando commitWrite() . I metodi beginRead / commitRead funzionano allo stesso modo.
  • I metodi beginRead / Write prendono come input il numero di messaggi da leggere/scrivere e restituiscono un booleano che indica se la lettura/scrittura è possibile. Se la lettura o la scrittura è possibile, la struttura memTx viene popolata con puntatori di base che possono essere utilizzati per l'accesso diretto del puntatore alla memoria condivisa del buffer circolare.
  • La struttura MemRegion contiene dettagli su un blocco di memoria, incluso il puntatore base (indirizzo base del blocco di memoria) e la lunghezza in termini di T (lunghezza del blocco di memoria in termini del tipo definito da HIDL della coda di messaggi).
  • La struttura MemTransaction contiene due strutture MemRegion , first e second poiché una lettura o una scrittura nel buffer dell'anello potrebbe richiedere un ritorno all'inizio della coda. Ciò significherebbe che sono necessari due puntatori di base per leggere/scrivere i dati nel buffer dell'anello FMQ.

Per ottenere l'indirizzo di base e la lunghezza da una struttura 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

Per ottenere riferimenti al primo e al secondo MemRegion all'interno di un oggetto MemTransaction :

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

Esempio di scrittura su FMQ utilizzando API a copia zero:

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
}

Fanno parte di MemTransaction anche i seguenti metodi di supporto:

  • T* getSlot(size_t idx);
    Restituisce un puntatore allo slot idx all'interno delle MemRegions che fanno parte di questo oggetto MemTransaction . Se l'oggetto MemTransaction rappresenta le regioni di memoria per leggere/scrivere N elementi di tipo T, l'intervallo valido di idx è compreso tra 0 e N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Scrivere elementi nMessages di tipo T nelle regioni di memoria descritte dall'oggetto, a partire dall'indice startIdx . Questo metodo utilizza memcpy() e non deve essere utilizzato per un'operazione di copia zero. Se l'oggetto MemTransaction rappresenta la memoria per leggere/scrivere N elementi di tipo T, l'intervallo valido di idx è compreso tra 0 e N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Metodo helper per leggere elementi nMessages di tipo T dalle aree di memoria descritte dall'oggetto a partire da startIdx . Questo metodo utilizza memcpy() e non è pensato per essere utilizzato per un'operazione di copia zero.

Invio della coda su HIDL

Dal lato della creazione:

  1. Creare un oggetto coda di messaggi come descritto sopra.
  2. Verificare che l'oggetto sia valido con isValid() .
  3. Se rimarrai in attesa su più code passando un EventFlag nella forma lunga di readBlocking() / writeBlocking() , puoi estrarre il puntatore del flag di evento (usando getEventFlagWord() ) da un oggetto MessageQueue che è stato inizializzato per creare il flag, e utilizzare quel flag per creare l'oggetto EventFlag necessario.
  4. Utilizzare il metodo MessageQueue getDesc() per ottenere un oggetto descrittore.
  5. Nel file .hal , fornire al metodo un parametro di tipo fmq_sync o fmq_unsync dove T è un tipo adatto definito da HIDL. Utilizzarlo per inviare l'oggetto restituito da getDesc() al processo ricevente.

Dal lato ricevente:

  1. Utilizzare l'oggetto descrittore per creare un oggetto MessageQueue . Assicurati di utilizzare lo stesso tipo di coda e tipo di dati, altrimenti il ​​modello non verrà compilato.
  2. Se hai estratto un flag di evento, estrai il flag dall'oggetto MessageQueue corrispondente nel processo di ricezione.
  3. Utilizzare l'oggetto MessageQueue per trasferire i dati.