Fila de mensagens rápidas (FMQ)

A infraestrutura de chamada de procedimento remoto (RPC) do HIDL usa mecanismos Binder, o que significa que as chamadas envolvem sobrecarga, exigem operações do kernel e podem acionar a ação do agendador. No entanto, para casos em que os dados devem ser transferidos entre processos com menos sobrecarga e sem envolvimento do kernel, o sistema Fast Message Queue (FMQ) é usado.

O FMQ cria filas de mensagens com as propriedades desejadas. Um objeto MQDescriptorSync ou MQDescriptorUnsync pode ser enviado por meio de uma chamada HIDL RPC e usado pelo processo de recebimento para acessar a fila de mensagens.

As filas de mensagens rápidas são suportadas apenas em C++ e em dispositivos que executam o Android 8.0 e superior.

Tipos de MessageQueue

O Android oferece suporte a dois tipos de fila (conhecidos como sabores ):

  • Filas não sincronizadas podem transbordar e podem ter muitos leitores; cada leitor deve ler os dados a tempo ou perdê-los.
  • As filas sincronizadas não têm permissão para estourar e podem ter apenas um leitor.

Ambos os tipos de fila não têm permissão para underflow (a leitura de uma fila vazia falhará) e só podem ter um gravador.

Não sincronizado

Uma fila não sincronizada tem apenas um gravador, 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 de sua própria posição de leitura independente.

As gravações na fila sempre são bem-sucedidas (não são verificadas quanto a estouro), desde que não sejam maiores que a capacidade da fila configurada (gravações maiores que a capacidade da fila falham imediatamente). Como cada leitor pode ter uma posição de leitura diferente, em vez de esperar que cada leitor leia todos os dados, os dados podem sair da fila sempre que novas gravações precisarem do espaço.

As leituras 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á disponível falha imediatamente (se não estiver bloqueando) ou espera que dados suficientes estejam disponíveis (se estiver bloqueando). 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 seja maior que a capacidade da fila, a próxima leitura não retornará dados; em vez disso, ele redefine a posição de leitura do leitor para igualar a última posição de gravação e retorna a falha. Se os dados disponíveis para leitura forem verificados após o estouro, mas antes da próxima leitura, ele mostrará mais dados disponíveis para leitura do que a capacidade da fila, indicando que ocorreu um estouro. (Se a fila estourar entre a verificação de dados disponíveis e a tentativa de ler esses dados, a única indicação de estouro é que a leitura falha.)

Sincronizado

Uma fila sincronizada tem um gravador 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 atualmente. Dependendo se a função de gravação ou leitura de bloqueio ou não-bloqueio é chamada, as tentativas de exceder o espaço ou os dados disponíveis retornam a falha imediatamente 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 falharão imediatamente.

Configurando um FMQ

Uma fila de mensagens requer vários objetos MessageQueue : um para ser gravado e um ou mais para serem lidos. Não há configuração explícita de qual objeto é usado para escrita ou leitura; cabe ao usuário garantir que nenhum objeto seja usado para leitura e escrita, que haja no máximo um gravador e, para filas sincronizadas, que haja no máximo um leitor.

Criando 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 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 */);
  • O inicializador MessageQueue<T, flavor>(numElements) cria e inicializa um objeto que dá suporte à funcionalidade de fila de mensagens.
  • O inicializador MessageQueue<T, flavor>(numElements, configureEventFlagWord) cria e inicializa um objeto que dá suporte à funcionalidade de fila de mensagens com bloqueio.
  • O tipo pode ser flavor para uma fila kUnsynchronizedWrite kSynchronizedReadWrite uma fila não sincronizada.
  • uint16_t (neste exemplo) pode ser qualquer tipo definido por HIDL que não envolva buffers aninhados (sem tipos de string ou vec ), identificadores ou interfaces.
  • kNumElementsInQueue indica o tamanho da fila em número de entradas; ele determina o tamanho do buffer de memória compartilhada que será alocado para a fila.

Criando 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 HIDL RPC para o processo que conterá a segunda extremidade da fila de mensagens. O MQDescriptor contém informações sobre a fila, incluindo:

  • Informações para mapear o buffer e escrever o ponteiro.
  • Informações para mapear o ponteiro de leitura (se a fila estiver sincronizada).
  • Informações para mapear a palavra do sinalizador de evento (se a fila estiver bloqueando).
  • Tipo de objeto ( <T, flavor> ), que inclui o tipo de elementos de fila definido por HIDL e o tipo de fila (sincronizado ou não sincronizado).

O objeto MQDescriptor pode ser usado para construir 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 devem ser 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 controle extra sobre a memória compartilhada, você pode configurar o MQDescriptor manualmente ( MQDescriptor é definido em system/libhidl/base/include/hidl/MQDescriptor.h ) e depois criar cada objeto MessageQueue conforme descrito nesta seção.

Bloqueando filas e sinalizadores de eventos

Por padrão, as filas não suportam o bloqueio de leituras/gravações. Existem dois tipos de bloqueio de chamadas de leitura/gravação:

  • Forma curta , com três parâmetros (ponteiro de dados, número de itens, tempo limite). Suporta bloqueio em operações individuais de leitura/gravação em uma única fila. Ao usar este formulário, a fila manipulará o sinalizador de evento e as máscaras de bits internamente, e o primeiro objeto da fila de mensagens deve ser inicializado com um segundo parâmetro true . Por exemplo:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Long form, with six parameters (includes event flag and bitmasks). Supports using a shared EventFlag object between multiple queues and allows specifying the notification bit masks to be used. In this case, the event flag and bitmasks must be supplied to each read and write call.

For the long form, the EventFlag can be supplied explicitly in each readBlocking() and writeBlocking() call. One of the queues may be initialized with an internal event flag, which must then be extracted from that queue's MessageQueue objects using getEventFlagWord() and used to create EventFlag objects in each process for use with other FMQs. Alternatively, the EventFlag objects can be initialized with any suitable shared memory.

In general, each queue should use only one of non-blocking, short-form blocking, or long-form blocking. It is not an error to mix them, but careful programming is required to get the desired result.

Using the MessageQueue

The public API of the MessageQueue object is:

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() podem ser usados ​​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 seu 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 estouro falhará e resultará na posição de leitura desse leitor igual ao ponteiro de gravação atual, independentemente de o estouro ter sido relatado por availableToRead() .

Os métodos read() e write() retornam true se todos os dados solicitados puderem ser (e foram) transferidos de/para a fila. Esses métodos não bloqueiam; eles são bem-sucedidos (e retornam true ) ou retornam falha ( false ) imediatamente.

Os readBlocking() e writeBlocking() aguardam até que a operação solicitada possa ser concluída ou até atingirem o tempo limite (um valor timeOutNanos de 0 significa que nunca expira).

As operações de bloqueio são implementadas usando uma palavra de sinalizador de evento. Por padrão, cada fila cria e usa sua própria palavra sinalizadora para suportar a forma abreviada de readBlocking() e writeBlocking() . É possível que várias filas compartilhem uma única palavra, para que um processo possa aguardar gravações ou leituras em qualquer uma das filas. Um ponteiro para a palavra do sinalizador de evento de uma fila pode ser obtido chamando getEventFlagWord() , e esse ponteiro (ou qualquer ponteiro para um local de memória compartilhado adequado) pode ser usado para criar um objeto EventFlag para passar para a forma longa de readBlocking() e writeBlocking() para uma fila diferente. Os parâmetros readNotification e writeNotification informam quais bits no sinalizador de evento devem ser usados ​​para sinalizar leituras e gravações nessa fila. readNotification e writeNotification são bitmasks de 32 bits.

readBlocking() espera nos bits writeNotification ; se esse parâmetro for 0, a chamada sempre falhará. Se o valor readNotification for 0, a chamada não falhará, mas uma leitura bem-sucedida não definirá nenhum bit de notificação. Em uma fila sincronizada, isso significaria que a chamada writeBlocking() correspondente nunca será ativada, a menos que o bit seja definido em outro lugar. Em uma fila não sincronizada, writeBlocking() não aguardará (ainda deve ser usado para definir o bit de notificação de gravação) e é apropriado que as leituras não definam nenhum bit de notificação. Da mesma forma, writeblocking() falhará se readNotification for 0 e uma gravação bem-sucedida definirá os bits writeNotification especificados.

Para aguardar em 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 então usadas para verificar se a fila correspondente tem espaço ou dados suficientes para a operação de gravação/leitura desejada e executar uma write() sem bloqueio/ read() . Para obter uma notificação pós-operação, use outra chamada para o método wake() do EventFlag . Para obter uma definição da abstração EventFlag , consulte system/libfmq/include/fmq/EventFlag.h .

Operações de cópia zero

As APIs read / write / readBlocking / writeBlocking() pegam um ponteiro para um buffer de entrada/saída como um argumento e usam chamadas memcpy() internamente para copiar dados entre o mesmo e o buffer de anel FMQ. Para melhorar o desempenho, o Android 8.0 e superior incluem um conjunto de APIs que fornecem acesso direto do ponteiro ao buffer de anel, eliminando a necessidade de usar chamadas memcpy .

Use as seguintes APIs públicas para operações de FMQ de cópia 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);
  • O método beginWrite fornece ponteiros básicos para o buffer de anel FMQ. Depois que os dados forem gravados, confirme-os usando commitWrite() . Os métodos beginRead / commitRead agem da mesma maneira.
  • Os métodos beginRead / Write recebem como entrada o número de mensagens a serem lidas/gravadas e retornam um booleano indicando se a leitura/gravação é possível. Se a leitura ou gravação for possível, a estrutura memTx é preenchida com ponteiros base que podem ser usados ​​para acesso direto ao ponteiro na memória compartilhada do buffer de anel.
  • 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 de T (comprimento do bloco de memória em termos do tipo definido por HIDL da fila de mensagens).
  • O struct MemTransaction contém dois structs MemRegion , first e second , pois uma leitura ou gravação no buffer de anel pode exigir uma quebra ao redor do início da fila. Isso significaria que dois ponteiros base são necessários para ler/gravar dados no buffer de anel FMQ.

Para obter o endereço base e o comprimento de uma estrutura 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 obter referências ao primeiro e segundo MemRegion s 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 no FMQ usando APIs de cópia 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
}

Os seguintes métodos auxiliares também fazem parte de MemTransaction :

  • T* getSlot(size_t idx);
    Retorna um ponteiro para o slot idx dentro das MemRegions que fazem parte deste objeto MemTransaction . Se o objeto MemTransaction estiver representando as regiões de memória para ler/gravar N itens do tipo T, o intervalo válido de idx estará entre 0 e N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Escreva itens nMessages do tipo T nas regiões de memória descritas pelo objeto, começando no índice startIdx . Este método usa memcpy() e não deve ser usado para uma operação de cópia zero. Se o objeto MemTransaction representar memória para ler/gravar N itens do tipo T, o intervalo válido de idx estará entre 0 e N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Método auxiliar para ler itens nMessages do tipo T das regiões de memória descritas pelo objeto a partir de startIdx . Esse método usa memcpy() e não deve ser usado para uma operação de cópia zero.

Enviando a fila por HIDL

Do lado da criação:

  1. Crie o objeto de fila de mensagens conforme descrito acima.
  2. Verifique se o objeto é válido com isValid() .
  3. Se você estiver esperando em várias filas passando um EventFlag no formato longo de readBlocking() / writeBlocking() , você pode extrair o ponteiro do sinalizador de evento (usando getEventFlagWord() ) de um objeto MessageQueue que foi inicializado para criar o sinalizador, e use esse sinalizador para criar o objeto EventFlag necessário.
  4. Use o método MessageQueue getDesc() para obter um objeto descritor.
  5. No arquivo .hal , dê ao método um parâmetro do tipo fmq_sync ou fmq_unsync onde T é um tipo definido por HIDL adequado. Use isso para enviar o objeto retornado por getDesc() para o processo de recebimento.

Do lado receptor:

  1. Use o objeto descritor para criar um objeto MessageQueue . Certifique-se de usar o mesmo tipo de fila e tipo de dados, ou o modelo não será compilado.
  2. Se você extraiu um sinalizador de evento, extraia o sinalizador do objeto MessageQueue correspondente no processo de recebimento.
  3. Use o objeto MessageQueue para transferir dados.