Coda di messaggi rapida (FMQ)

L'infrastruttura di chiamata di procedura remota (RPC) di HIDL utilizza meccanismi di binder, il che significa che le chiamate comportano un overhead, richiedono operazioni del kernel e possono attivare l'azione dello scheduler. Tuttavia, nei casi in cui i dati devono essere trasferiti tra processi con meno overhead e senza coinvolgimento del kernel, viene utilizzato il sistema Fast Message Queue (FMQ).

FMQ crea code di messaggi con le proprietà desiderate. Puoi inviare un oggetto MQDescriptorSync o MQDescriptorUnsync tramite una chiamata RPC HIDL e l'oggetto viene utilizzato dal processo di ricezione per accedere alla coda dei messaggi.

Tipi di coda

Android supporta due tipi di code (chiamati flavor):

  • Le code non sincronizzate possono superare il limite e possono avere molti lettori; ogni lettore deve leggere i dati in tempo o perderli.
  • Le code sincronizzate non possono superare il limite e possono avere un solo lettore.

Per entrambi i tipi di coda non è consentito il sottoflusso (la lettura da una coda vuota non va a buon fine) e possono avere un solo autore.

Code non sincronizzate

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

Le scritture nella coda vanno sempre a buon fine (non viene controllato se si verifica un overflow) a condizione che non superino la capacità della coda configurata (le scritture superiori alla capacità della coda non vanno a buon fine immediatamente). Poiché ogni lettore potrebbe avere una posizione di lettura diversa, anziché attendere che ogni lettore legga ogni dato, i dati vengono rimossi dalla coda ogni volta che le nuove scritture richiedono spazio.

È responsabilità dei lettori recuperare i dati prima che vengano eliminati dalla coda. Una lettura che tenta di leggere più dati di quelli disponibili non va a buon fine immediatamente (se non bloccante) o attende che siano disponibili dati sufficienti (se bloccante). Una lettura che tenta di leggere più dati rispetto alla capacità della coda non va mai a buon fine immediatamente.

Se un lettore non riesce a tenere il passo con lo scrittore, in modo che la quantità di dati scritta e non ancora letta dal lettore superi la capacità della coda, la lettura successiva non restituisce dati; reimposta invece la posizione di lettura del lettore sulla posizione di scrittura più la metà della capacità e restituisce un errore. In questo modo, metà del buffer è disponibile per la lettura e viene riservato spazio per le nuove scritture per evitare di svuotare nuovamente la coda. Se i dati disponibili per la lettura vengono controllati dopo un overflow, ma prima della lettura successiva, vengono visualizzati più dati disponibili per la lettura rispetto alla capacità della coda, a indicare che si è verificato un overflow. Se la coda supera il limite tra il controllo dei dati disponibili e il tentativo di lettura, l'unica indicazione di overflow è che la lettura non va a buon fine.

Code sincronizzate

Una coda sincronizzata ha un autore e un lettore con una singola posizione di scrittura e una singola posizione di lettura. È impossibile scrivere più dati di quanto la coda possa contenere o leggere più dati di quelli attualmente memorizzati nella coda. A seconda che venga chiamata la funzione di scrittura o lettura bloccante o non bloccante, i tentativi di superare lo spazio o i dati disponibili restituiscono immediatamente un errore o si bloccano fino al completamento dell'operazione desiderata. I tentativi di leggere o scrivere più dati rispetto alla capacità della coda non vanno mai a buon fine.

Configurare 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 dell'oggetto utilizzato per la scrittura o la lettura. L'utente è responsabile di garantire che non venga utilizzato alcun oggetto sia per la lettura sia per la scrittura, che esista al massimo un autore e, per le code sincronizzate, che esista al massimo un lettore.

Crea il primo oggetto MessageQueue

Viene creata e configurata una coda di messaggi 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 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 */);
  • L'inizializzatore MessageQueue<T, flavor>(numElements) crea e inizializza un oggetto che supporta la funzionalità della coda di messaggi.
  • L'inizializzatore MessageQueue<T, flavor>(numElements, configureEventFlagWord) crea e inizializza un oggetto che supporta la funzionalità di coda dei messaggi con 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 implichi 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 della memoria condivisa allocata per la coda.

Crea il 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 gestisce la seconda estremità della coda dei messaggi. MQDescriptor contiene informazioni sulla coda, tra cui:

  • Informazioni per mappare il buffer e il puntatore di scrittura.
  • Informazioni per mappare l'indicatore 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 definito da HIDL degli elementi della coda e il tipo di coda (sincronizzata o non sincronizzata).

Puoi utilizzare l'oggetto MQDescriptor per creare 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 (locale per ogni oggetto MessageQueue nelle code non sincronizzate) viene sempre impostata su 0 durante la creazione. In genere, MQDescriptor viene inizializzato durante la creazione del primo oggetto coda di messaggi. Per un maggiore controllo sulla memoria condivisa, puoi configurare manualmente MQDescriptor (MQDescriptor è definito in system/libhidl/base/include/hidl/MQDescriptor.h), quindi creare ogni oggetto MessageQueue come descritto in questa sezione.

Code di blocco e flag evento

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

  • Il formato breve, con tre parametri (puntatore dati, numero di elementi, timeout), supporta il blocco delle singole operazioni di lettura e scrittura su una singola coda. Quando utilizzi questo modulo, la coda gestisce internamente il flag evento e le maschere di bit e l'oggetto coda del primo messaggio deve essere inizializzato con un secondo parametro di true. Ad esempio:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Il formato lungo, con sei parametri (inclusi flag 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 ogni chiamata di lettura e scrittura.

Per il formato lungo, puoi fornire EventFlag esplicitamente in ogni chiamata readBlocking() e writeBlocking(). Puoi inizializzare una delle code con un flag evento interno, che deve poi essere estratto dagli oggetti MessageQueue della coda utilizzando getEventFlagWord() e utilizzato per creare oggetti EventFlag in ogni processo da utilizzare con altre code FMQ. In alternativa, puoi inizializzare gli oggetti EventFlag con qualsiasi memoria condivisa adatta.

In generale, ogni coda deve utilizzare una sola modalità di blocco: non bloccante, blocco dei contenuti nel formato breve o blocco dei contenuti nel formato lungo. Non è un errore combinarli, ma è necessaria una programmazione attenta per ottenere il risultato desiderato.

Contrassegna la memoria come di sola lettura

Per impostazione predefinita, la memoria condivisa dispone delle autorizzazioni di lettura e scrittura. Per le code non sincronizzate (kUnsynchronizedWrite), lo scrittore potrebbe voler rimuovere le autorizzazioni di scrittura per tutti i lettori prima di distribuire gli oggetti MQDescriptorUnsync. In questo modo, gli altri processi non possono scrivere nella coda, il che è consigliabile per proteggersi da bug o comportamenti errati nei processi di lettura. Se lo scrittore vuole che i lettori possano 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 utenti di questa coda, il loro codice deve essere modificato per creare la coda con resetPointer=false.

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

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

Puoi utilizzare availableToWrite() e availableToRead() per determinare la quantità di dati che può essere trasferita in un'unica operazione. In una coda non sincronizzata:

  • availableToWrite() restituisce sempre la capacità della coda.
  • Ogni lettore ha la propria posizione di lettura ed esegue il proprio calcolo per availableToRead().
  • Dal punto di vista di un lettore lento, la coda può essere sovraccaricata. Ciò può comportare il ritorno di un valore maggiore delle dimensioni della coda da parte di availableToRead(). La prima lettura dopo un overflow non va a buon fine e comporta l'impostazione della posizione di lettura per quel lettore 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 possono essere (e sono stati) trasferiti da e verso la coda. Questi metodi non si bloccano, ma vanno a buon fine (e restituiscono true) o restituiscono immediatamente un errore (false).

I metodi readBlocking() e writeBlocking() rimangono in attesa fino a quando non è possibile completare l'operazione richiesta o fino al timeout (un valore timeOutNanos pari a 0 indica che non si verifica mai il timeout).

Le operazioni di blocco vengono implementate utilizzando una parola di indicatore di evento. Per impostazione predefinita, ogni coda crea e utilizza la propria parola di indicatore per supportare la forma abbreviata di readBlocking() e writeBlocking(). Più codine possono condividere una singola parola, in modo che un processo possa attendere le scritture o le letture in una delle codine. Chiamando getEventFlagWord(), puoi ottenere un puntatore alla parola di flag evento di una coda e puoi utilizzare questo puntatore (o qualsiasi altro puntatore a una posizione della memoria condivisa appropriata) per creare un oggetto EventFlag da passare al formato lungo di readBlocking() e writeBlocking() per un'altra coda. I parametri readNotification e writeNotification indicano quali bit del flag evento devono essere utilizzati per segnalare le letture e le scritture nella coda. readNotification e writeNotification sono maschere di bit a 32 bit.

readBlocking() attende i bit writeNotification. Se il parametro è 0, la chiamata non va mai a buon fine. Se il valore readNotification è 0, la chiamata non fallisce, ma una lettura riuscita non imposta alcun bit di notifica. In una coda sincronizzata, ciò significa che la chiamata writeBlocking() corrispondente non si attiva mai, a meno che il bit non sia impostato altrove. In una coda non sincronizzata, writeBlocking() non attende (deve comunque essere utilizzato per impostare il bit di notifica di scrittura) ed è opportuno che le letture non imposti alcun bit di notifica. Analogamente, writeblocking() non va a buon fine se readNotification è 0 e una scrittura riuscita imposta i bit writeNotification specificati.

Per attendere in più code contemporaneamente, utilizza 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 di riattivazione. Queste informazioni vengono poi utilizzate per verificare che la coda corrispondente abbia spazio o dati sufficienti per l'operazione di scrittura e lettura desiderata ed eseguire write() e read() non bloccanti. Per ricevere una notifica post-operazione, utilizza un'altra chiamata al metodo wake() dell'oggetto EventFlag. Per una definizione dell'astrazione EventFlag, consulta system/libfmq/include/fmq/EventFlag.h.

Operazioni zero copy

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

Utilizza le seguenti API pubbliche per le operazioni FMQ senza copia:

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 gli indicatori di base nel buffer ad anello FMQ. Dopo aver scritto i dati, esegui il commit utilizzando commitWrite(). I metodi beginRead e commitRead funzionano allo stesso modo.
  • I metodi beginRead e Write prendono in input il numero di messaggi da leggere e scrivere e restituiscono un valore booleano che indica se la lettura o la scrittura è possibile. Se la lettura o la scrittura è possibile, la struttura memTx viene compilata con puntatori di base che possono essere utilizzati per l'accesso diretto con puntatore alla memoria condivisa dell'anello.
  • La struct MemRegion contiene i dettagli di un blocco di memoria, tra cui il puntatore di base (indirizzo di base del blocco di memoria) e la lunghezza in termini di T (lunghezza del blocco di memoria in termini di tipo della coda di messaggi definito da HIDL).
  • Lo struct MemTransaction contiene due struct MemRegion, first e second, poiché una lettura o una scrittura nel buffer circolare potrebbe richiedere un a capo all'inizio della coda. Ciò significa che sono necessari due puntatori di base per leggere e scrivere dati nell'anello buffer FMQ.

Per ottenere l'indirizzo 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 i riferimenti alla prima e alla seconda struct 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 nella coda FMQ utilizzando le API di 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
}

I seguenti metodi di assistenza fanno parte anche di MemTransaction:

  • T* getSlot(size_t idx); restituisce un puntatore allo slot idx all'interno di MemRegions che fanno parte di questo oggetto MemTransaction. Se l'oggetto MemTransaction rappresenta le regioni di memoria per leggere e 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); scrive nMessages elementi di tipo T nelle regioni di memoria descritte dall'oggetto, a partire dall'indice startIdx. Questo metodo utilizza memcpy() e non è destinato all'uso per un'operazione senza copia. Se l'oggetto MemTransaction rappresenta la memoria per leggere e 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); è un metodo di supporto per leggere gli elementi nMessages di tipo T dalle regioni di memoria descritte dall'oggetto a partire da startIdx. Questo metodo utilizza memcpy() e non è pensato per essere utilizzato per un'operazione zero copy.

Invia la coda tramite HIDL

Per quanto riguarda la creazione:

  1. Crea un oggetto coda di messaggi come descritto sopra.
  2. Verifica che l'oggetto sia valido con isValid().
  3. Se stai aspettando in più code passando EventFlag al formato lungo di readBlocking() o writeBlocking(), puoi estrarre il puntatore del flag evento (utilizzando getEventFlagWord()) da un oggetto MessageQueue inizializzato per creare il flag e utilizzare questo flag per creare l'oggetto EventFlag necessario.
  4. Utilizza il metodo MessageQueue getDesc() per ottenere un oggetto descrittore.
  5. Nel file HAL, assegna al metodo un parametro di tipo fmq_sync o fmq_unsync, dove T è un tipo definito da HIDL adatto. Utilizzalo per inviare l'oggetto restituito da getDesc() al processo di ricezione.

Sul lato di ricezione:

  1. Utilizza l'oggetto descrittore per creare un oggetto MessageQueue. Utilizza lo stesso tipo di coda e di dati, altrimenti la compilazione del modello non andrà a buon fine.
  2. Se hai estratto un flag evento, estrailo dall'oggetto MessageQueue corrispondente nel processo di ricezione.
  3. Utilizza l'oggetto MessageQueue per trasferire i dati.