A infraestrutura de chamada de procedimento remoto (RPC) da HIDL usa mecanismos de vinculação, o que significa que as chamadas envolvem sobrecarga, exigem operações do kernel e podem acionar ações do agendador. No entanto, para casos em que os dados precisam ser transferidos entre processos com menos sobrecarga e sem envolvimento do kernel, o sistema de fila de mensagens rápidas (FMQ, na sigla em inglês) é usado.
O FMQ cria filas de mensagens com as propriedades desejadas. É possível enviar um objeto
MQDescriptorSync
ou MQDescriptorUnsync
por uma chamada de RPC HIDL, e o objeto é usado pelo processo de recebimento para acessar a
fila de mensagens.
Tipos de fila
O Android oferece suporte a dois tipos de fila (conhecidos como sabores):
- As filas não sincronizadas podem transbordar e ter muitos leitores. Cada leitor precisa ler os dados a tempo ou perdê-los.
- As filas sincronizadas não podem transbordar e podem ter apenas um leitor.
Não é permitido que ambos os tipos de fila tenham underflow (a leitura de uma fila vazia falha) e só podem ter um escritor.
Filas não sincronizadas
Uma fila não sincronizada tem apenas um escritor, mas pode ter qualquer número de leitores. Há uma posição de gravação para a fila. No entanto, cada leitor mantém o controle da própria posição de leitura independente.
As gravações na fila sempre são bem-sucedidas (não são verificadas para transbordamento) desde que não sejam maiores que a capacidade de fila configurada. As gravações maiores que a capacidade de fila falham imediatamente. Como cada leitor pode ter uma posição de leitura diferente, em vez de esperar que cada leitor leia cada parte dos dados, os dados saem da fila sempre que novas gravações precisam do espaço.
Os leitores são responsáveis por recuperar os dados antes que eles caiam no final da fila. Uma leitura que tenta ler mais dados do que estão disponíveis falha imediatamente (se não for de bloqueio) ou aguarda que dados suficientes estejam disponíveis (se for de bloqueio). Uma leitura que tenta ler mais dados do que a capacidade da fila sempre falha imediatamente.
Se um leitor não conseguir acompanhar o gravador, de modo que a quantidade de dados gravados e ainda não lidos por esse leitor exceda a capacidade da fila, a próxima leitura não vai retornar dados. Em vez disso, ela vai redefinir a posição de leitura do leitor para a posição de gravação mais metade da capacidade e, em seguida, retornar uma falha. Isso deixa metade do buffer disponível para leitura e reserva espaço para novas gravações para evitar o transbordamento imediato da fila. Se os dados disponíveis para leitura forem verificados após um overflow, mas antes da próxima leitura, mais dados disponíveis para leitura do que a capacidade da fila serão mostrados, indicando que um overflow ocorreu. Se a fila transbordar entre a verificação dos dados disponíveis e a tentativa de leitura desses dados, a única indicação de overflow será a falha na leitura.
Filas sincronizadas
Uma fila sincronizada tem um escritor e um leitor com uma única posição de gravação e uma única posição de leitura. É impossível gravar mais dados do que a fila tem espaço ou ler mais dados do que a fila contém. Dependendo de se a função de leitura ou gravação bloqueada ou não bloqueada é chamada, as tentativas de exceder o espaço ou os dados disponíveis retornam uma falha imediata ou bloqueiam até que a operação desejada possa ser concluída. As tentativas de ler ou gravar mais dados do que a capacidade da fila sempre falham imediatamente.
Configurar uma FMQ
Uma fila de mensagens requer vários objetos MessageQueue
: um para
ser gravado e um ou mais para serem lidos. Não há uma configuração
explícita de qual objeto é usado para gravação ou leitura. O usuário é responsável por
garantir que nenhum objeto seja usado para leitura e gravação, que haja
no máximo um gravador e, para filas sincronizadas, que haja no máximo um
leitor.
Criar o primeiro objeto MessageQueue
Uma fila de mensagens é criada e configurada com uma única chamada:
#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 */);
- O inicializador
MessageQueue<T, flavor>(numElements)
cria e inicializa um objeto que oferece suporte à funcionalidade de fila de mensagens. - O inicializador
MessageQueue<T, flavor>(numElements, configureEventFlagWord)
cria e inicializa um objeto que oferece suporte à funcionalidade de fila de mensagens com bloqueio. flavor
pode serkSynchronizedReadWrite
para uma fila sincronizada oukUnsynchronizedWrite
para uma fila assíncrona.uint16_t
(neste exemplo) pode ser qualquer tipo definido por HIDL que não envolva buffers aninhados (sem tiposstring
ouvec
), identificadores ou interfaces.kNumElementsInQueue
indica o tamanho da fila em número de entradas. Ele determina o tamanho do buffer de memória compartilhada alocado para a fila.
Criar o segundo objeto MessageQueue
O segundo lado da fila de mensagens é criado usando um
objeto MQDescriptor
obtido do primeiro lado. O
objeto MQDescriptor
é enviado por uma chamada RPC HIDL ou AIDL para o processo
que contém o segundo fim da fila de mensagens. O
MQDescriptor
contém informações sobre a fila, incluindo:
- Informações para mapear o buffer e o ponteiro de gravação.
- Informações para mapear o ponteiro de leitura (se a fila estiver sincronizada).
- Informações para mapear a palavra de indicador de evento (se a fila estiver bloqueando).
- Tipo de objeto (
<T, flavor>
), que inclui o tipo definido por HIDL de elementos de fila e o tipo de fila (sincronizado ou não).
É possível usar o objeto MQDescriptor
para criar um objeto
MessageQueue
:
MessageQueue<T, flavor>::MessageQueue(const MQDescriptor<T, flavor>& Desc, bool resetPointers)
O parâmetro resetPointers
indica se as posições de leitura
e gravação serão redefinidas para 0 ao criar esse objeto MessageQueue
.
Em uma fila não sincronizada, a posição de leitura (que é local para cada
objeto MessageQueue
em filas não sincronizadas) é sempre definida como 0
durante a criação. Normalmente, o MQDescriptor
é inicializado durante
a criação do primeiro objeto de fila de mensagens. Para ter mais controle sobre a memória
compartilhada, configure o MQDescriptor
manualmente
(MQDescriptor
é definido em
system/libhidl/base/include/hidl/MQDescriptor.h
)
e crie todos os objetos MessageQueue
, conforme descrito nesta seção.
Bloquear filas e flags de eventos
Por padrão, as filas não permitem bloquear leituras e gravações. Há dois tipos de chamadas de leitura e gravação de bloqueio:
- O formato curto, com três parâmetros (ponteiro de dados, número de itens e
tempo limite), oferece suporte ao bloqueio de operações de leitura e gravação individuais em uma única
fila. Ao usar esse formulário, a fila processa a flag de evento e as máscaras de bits
internamente, e o primeiro objeto de fila de mensagens precisa
ser inicializado com um segundo parâmetro de
true
. Exemplo:// For an unsynchronized FMQ that supports blocking mFmqUnsynchronizedBlocking = new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite> (kNumElementsInQueue, true /* enable blocking operations */);
- O formato longo, com seis parâmetros (incluindo sinalizador de evento e máscaras de bits),
oferece suporte ao uso de um objeto
EventFlag
compartilhado entre várias filas e permite especificar as máscaras de bits de notificação a serem usadas. Nesse caso, a flag de evento e as bitmasks precisam ser fornecidas para cada chamada de leitura e gravação.
No formato longo, é possível fornecer o EventFlag
explicitamente em
cada chamada readBlocking()
e writeBlocking()
. É possível inicializar uma das
filas com uma flag de evento interno, que precisa ser
extraída dos objetos MessageQueue
dessa fila usando
getEventFlagWord()
e usada para criar um objeto EventFlag
em cada processo para uso com outras FMQs. Como alternativa, é possível inicializar os
objetos EventFlag
com qualquer memória compartilhada adequada.
Em geral, cada fila precisa usar apenas um tipo de bloqueio: não bloqueio, bloqueio de formato curto ou bloqueio de formato longo. Não é um erro misturá-los, mas é necessária uma programação cuidadosa para obter o resultado desejado.
Marcar a memória como somente leitura
Por padrão, a memória compartilhada tem permissões de leitura e gravação. Para filas
não sincronizadas (kUnsynchronizedWrite
), o gravador pode remover as permissões de gravação de todos
os leitores antes de distribuir os objetos MQDescriptorUnsync
. Isso garante que os outros
processos não possam gravar na fila, o que é recomendado para proteger contra bugs ou comportamento incorreto nos
processos do leitor.
Se o autor quiser que os leitores possam redefinir a fila sempre que usarem
MQDescriptorUnsync
para criar o lado de leitura da fila, a memória não poderá ser marcada
como somente leitura. Esse é o comportamento padrão do construtor MessageQueue
. Portanto, se
houver usuários dessa fila, o código deles precisará ser alterado para construir a fila com
resetPointer=false
.
- Gravador: chame
ashmem_set_prot_region
com um descritor de arquivoMQDescriptor
e a região definida como somente leitura (PROT_READ
):int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
- Leitor: crie uma fila de mensagens com
resetPointer=false
(o padrão étrue
):mFmq = new (std::nothrow) MessageQueue(mqDesc, false);
Usar a MessageQueue
A API pública do objeto 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);
É possível usar availableToWrite()
e availableToRead()
para determinar quantos dados podem ser transferidos em uma única operação. Em uma
fila não sincronizada:
availableToWrite()
sempre retorna a capacidade da fila.- Cada leitor tem sua própria posição de leitura e faz o próprio cálculo para
availableToRead()
. - Do ponto de vista de um leitor lento, a fila pode transbordar.
Isso pode resultar em
availableToRead()
retornando um valor maior que o tamanho da fila. A primeira leitura após um overflow falha e resulta na posição de leitura do leitor sendo definida como igual ao ponteiro de gravação atual, mesmo que o overflow tenha sido informado poravailableToRead()
.
Os métodos read()
e write()
retornam
true
se todos os dados solicitados puderem ser (e foram) transferidos para e da
fila. Esses métodos não bloqueiam. Eles podem ser bem-sucedidos (e retornar
true
) ou falhar (false
) imediatamente.
Os métodos readBlocking()
e writeBlocking()
aguardam
até que a operação solicitada possa ser concluída ou até que o tempo limite expire. Um
valor de timeOutNanos
igual a 0 significa que o tempo limite nunca será atingido.
As operações de bloqueio são implementadas usando uma palavra de indicador de evento. Por padrão,
cada fila cria e usa a própria palavra de sinalização para oferecer suporte à forma abreviada de
readBlocking()
e writeBlocking()
. Várias
filas podem compartilhar uma única palavra, para que um processo possa aguardar gravações ou
leituras em qualquer uma das filas. Ao chamar getEventFlagWord()
, você pode receber um ponteiro
para a palavra de flag de evento de uma fila e usar esse ponteiro (ou qualquer
ponteiro para um local de memória compartilhada adequado) para criar um
objeto EventFlag
a ser transmitido para o formulário longo de
readBlocking()
e writeBlocking()
para uma fila
diferente. Os parâmetros readNotification
e writeNotification
informam quais bits na flag de evento precisam ser usados para sinalizar leituras e
gravações nessa fila. readNotification
e
writeNotification
são máscaras de bits de 32 bits.
readBlocking()
aguarda os bits writeNotification
.
Se esse parâmetro for 0, a chamada sempre falhará. Se o
valor de readNotification
for 0, a chamada não vai falhar, mas uma
leitura bem-sucedida não vai definir nenhum bit de notificação. Em uma fila sincronizada,
isso significa que a chamada writeBlocking()
correspondente
nunca é ativada, a menos que o bit seja definido em outro lugar. Em uma fila não sincronizada,
writeBlocking()
não espera (ele ainda precisa ser usado para definir o
bit de notificação de gravação) e é adequado para leituras que não definem
bits de notificação. Da mesma forma, writeblocking()
falha se
readNotification
for 0, e uma gravação bem-sucedida define os bits
writeNotification
especificados.
Para aguardar várias filas de uma vez, use o método
wait()
de um objeto EventFlag
para aguardar uma máscara de bits de notificações. O
método wait()
retorna uma palavra de status com os bits que causaram o
conjunto de ativação. Essas informações são usadas para verificar se a fila correspondente tem
espaço ou dados suficientes para a operação de gravação e leitura desejada e realizar uma
write()
e read()
não bloqueante. Para receber uma notificação
pós-operação, use outra chamada para o método
wake()
do objeto EventFlag
. Para uma definição da abstração
EventFlag
, consulte
system/libfmq/include/fmq/EventFlag.h
.
Zero operações de cópia
Os métodos
read
, write
, readBlocking
e writeBlocking()
usam um ponteiro para um buffer de entrada/saída como argumento e usam
chamadas memcpy()
internamente para copiar dados entre o mesmo e o
buffer circular FMQ. Para melhorar o desempenho, o Android 8.0 e versões mais recentes incluem um conjunto de
APIs que fornecem acesso direto ao ponteiro no buffer circular, eliminando a
necessidade de usar chamadas memcpy
.
Use as APIs públicas a seguir para operações de FMQ sem cópia:
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);
- O método
beginWrite
fornece ponteiros básicos no buffer circular do FMQ. Depois que os dados forem gravados, confirme a gravação usandocommitWrite()
. Os métodosbeginRead
ecommitRead
funcionam da mesma maneira. - Os métodos
beginRead
eWrite
usam como entrada o número de mensagens a serem lidas e gravadas e retornam um booleano indicando se a leitura ou gravação é possível. Se a leitura ou gravação for possível, a structmemTx
será preenchida com ponteiros básicos que podem ser usados para acesso direto ao ponteiro na memória compartilhada do buffer circular. - A estrutura
MemRegion
contém detalhes sobre um bloco de memória, incluindo o ponteiro base (endereço base do bloco de memória) e o comprimento em termos deT
(comprimento do bloco de memória em termos do tipo definido pelo HIDL da fila de mensagens). - A estrutura
MemTransaction
contém duas estruturasMemRegion
,first
esecond
, porque uma leitura ou gravação no buffer em anel pode exigir um wraparound para o início da fila. Isso significa que dois ponteiros básicos são necessários para ler e gravar dados no buffer de anel do FMQ.
Para conseguir o endereço base e o comprimento de uma 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
Para receber referências à primeira e à segunda estrutura MemRegion
em um
objeto MemTransaction
:
const MemRegion& getFirstRegion(); // get a reference to the first MemRegion const MemRegion& getSecondRegion(); // get a reference to the second MemRegion
Exemplo de gravação na FMQ usando APIs de zero cópia:
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 }
Os métodos auxiliares a seguir também fazem parte de MemTransaction
:
T* getSlot(size_t idx);
retorna um ponteiro para o slotidx
dentro doMemRegions
que faz parte deste objetoMemTransaction
. Se o objetoMemTransaction
representar as regiões de memória para ler e gravar N itens do tipoT
, o intervalo válido deidx
estará entre 0 e N-1.bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
grava itensnMessages
do tipoT
nas regiões de memória descritas pelo objeto, começando pelo índicestartIdx
. Esse método usamemcpy()
e não deve ser usado para uma operação de cópia zero. Se o objetoMemTransaction
representar a memória para ler e gravar N itens do tipoT
, o intervalo válido deidx
estará entre 0 e N-1.bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
é um método auxiliar para ler itensnMessages
do tipoT
das regiões de memória descritas pelo objeto, começando emstartIdx
. Esse método usamemcpy()
e não é destinado a ser usado para uma operação de cópia zero.
Enviar a fila por HIDL
Do lado da criação:
- Crie um objeto de fila de mensagens conforme descrito acima.
- Verifique se o objeto é válido com
isValid()
. - Se você estiver esperando em várias filas transmitindo
EventFlag
para o formulário longo dereadBlocking()
ouwriteBlocking()
, extraia o indicador de flag de evento (usandogetEventFlagWord()
) de um objetoMessageQueue
que foi inicializado para criar a flag e use essa flag para criar o objetoEventFlag
necessário. - Use o método
MessageQueue
getDesc()
para receber um objeto de descritor. - No arquivo HAL, atribua ao método um parâmetro do tipo
fmq_sync
oufmq_unsync
, em queT
é um tipo adequado definido por HIDL. Use isso para enviar o objeto retornado porgetDesc()
ao processo de recebimento.
Do lado do destinatário:
- Use o objeto descritor para criar um objeto
MessageQueue
. Use o mesmo tipo de fila e de dados, ou o modelo não será compilado. - Se você extraiu uma flag de evento, extraia a flag do objeto
MessageQueue
correspondente no processo de recebimento. - Use o objeto
MessageQueue
para transferir dados.