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, per i 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 l'overflow 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 hanno sempre esito positivo (non sono verificate per l'overflow) purché non siano superiori alla capacità di coda configurata (le scritture superiori alla capacità della coda non superano immediatamente). Poiché ogni lettore potrebbe avere una posizione di lettura diversa, invece di attendere che ogni lettore legga ogni dato, i dati cadono dalla coda ogni volta che nuove scritture hanno bisogno di spazio.
I lettori sono responsabili del recupero dei 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 stare al passo con lo scrittore, in modo che la quantità di dati scritta e non ancora letta dal lettore sia maggiore della capacità della coda, la lettura successiva non restituisce dati; reimposta invece la posizione di lettura del lettore in modo che corrisponda all'ultima posizione di scrittura e restituisce un errore. Se i dati disponibili per la lettura vengono controllati dopo il sovraccarico, ma prima della lettura successiva, vengono visualizzati più dati disponibili per la lettura rispetto alla capacità della coda, a indicare che si è verificato un sovraccarico. (se la coda supera il controllo dei dati disponibili e il tentativo di leggere i dati, l'unica indicazione dell'overflow è che la lettura non riesce.)
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 quanti ne abbia spazio la coda o leggere più dati di quelli attualmente contenuti nella coda. A seconda che venga richiamata o meno la funzione di scrittura o lettura di blocco o non blocco, i tentativi di superare lo spazio disponibile o i dati restituiscono immediatamente l'errore o il blocco 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. È responsabilità dell'utente assicurarsi 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à di 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ò esserekSynchronizedReadWrite
per una coda sincronizzata okUnsynchronizedWrite
per una coda non sincronizzata.uint16_t
(in questo esempio) può essere qualsiasi tipo definito da HIDL che non implichi buffer nidificati (nessun tipostring
ovec
), handle o interfacce.kNumElementsInQueue
indica le dimensioni della coda in numero di voci e determina la dimensione del buffer di 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 contiene la seconda estremità della coda di 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 MQDescriptor
manualmente (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 accurata per ottenere il risultato desiderato.
Contrassegna il ricordo 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
. Ciò garantisce che gli altri
processi non possano scrivere nella coda, il che è consigliato per proteggerti da bug o comportamenti scadenti
nei processi del lettore.
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
.
- Writer: chiama
ashmem_set_prot_region
con un descrittore del fileMQDescriptor
e una regione impostati 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);
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 tramiteavailableToRead()
.
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()
attendono fino al completamento dell'operazione richiesta o fino al timeout (un valore timeOutNanos
pari a 0 significa che non scade mai).
Le operazioni di blocco vengono implementate utilizzando una parola di flag evento. Per impostazione predefinita, ogni coda crea e utilizza la propria parola di indicatore per supportare la forma abbreviata di readBlocking()
e writeBlocking()
. Più code possono condividere una singola parola, in modo che un processo possa attendere le scritture o le letture in una delle code. 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 nel flag di evento devono essere utilizzati per segnalare le letture e
le scritture in quella coda. readNotification
e
writeNotification
sono maschere di bit a 32 bit.
readBlocking()
attende i bit writeNotification
.
Se questo 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 è appropriato che le letture non impostino 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 quindi utilizzate per verificare che la coda corrispondente abbia spazio o dati sufficienti per l'operazione di scrittura e lettura desiderata ed eseguire le operazioni write()
e read()
che non bloccano. 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 utilizzandocommitWrite()
. I metodibeginRead
ecommitRead
funzionano allo stesso modo. - I metodi
beginRead
eWrite
assumono come input il numero di messaggi da leggere e scrivere e restituiscono un valore booleano che indica se è possibile leggere o scrivere. Se è possibile eseguire la lettura o la scrittura, lo structmemTx
viene compilato con puntatori di base che possono essere utilizzati per l'accesso diretto del puntatore nella memoria condivisa del buffer ad 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 diT
(lunghezza del blocco di memoria in termini di tipo della coda di messaggi definito da HIDL). - Lo struct
MemTransaction
contiene due structMemRegion
,first
esecond
perché 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 nel buffer ad anello di FMQ.
Per ottenere l'indirizzo di base e la lunghezza da uno 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
Per ottenere riferimenti al primo e al secondo 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 slotidx
all'interno diMemRegions
che fanno parte di questo oggettoMemTransaction
. Se l'oggettoMemTransaction
rappresenta le regioni di memoria per leggere e scrivere N elementi di tipoT
, l'intervallo valido diidx
è compreso tra 0 e N-1.bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
scrivenMessages
elementi di tipoT
nelle regioni di memoria descritte dall'oggetto, a partire dall'indicestartIdx
. Questo metodo utilizzamemcpy()
e non è destinato all'uso per un'operazione senza copia. Se l'oggettoMemTransaction
rappresenta la memoria per leggere e scrivere N elementi di tipoT
, l'intervallo valido diidx
è compreso tra 0 e N-1.bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
è un metodo di supporto per leggere gli elementinMessages
di tipoT
dalle regioni di memoria descritte dall'oggetto a partire dastartIdx
. Questo metodo utilizzamemcpy()
e non è destinato a essere utilizzato per un'operazione di zero copia.
Invia la coda tramite HIDL
Per quanto riguarda la creazione:
- Crea un oggetto coda di messaggi come descritto sopra.
- Verifica che l'oggetto sia valido con
isValid()
. - Se stai aspettando in più code passando
EventFlag
al formato lungo direadBlocking()
owriteBlocking()
, puoi estrarre il puntatore del flag evento (utilizzandogetEventFlagWord()
) da un oggettoMessageQueue
inizializzato per creare il flag e utilizzare questo flag per creare l'oggettoEventFlag
necessario. - Utilizza il metodo
MessageQueue
getDesc()
per ottenere un oggetto descrittore. - Nel file HAL, assegna al metodo un parametro di tipo
fmq_sync
ofmq_unsync
, doveT
è un tipo definito da HIDL adatto. Utilizzalo per inviare l'oggetto restituito dagetDesc()
al processo di ricezione.
Sul lato ricevente:
- 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. - Se hai estratto un flag evento, estrailo dall'oggetto
MessageQueue
corrispondente nel processo di ricezione. - Utilizza l'oggetto
MessageQueue
per trasferire i dati.