Fila de mensagens rápidas (FMQ, na sigla em inglês)

Se estiver procurando suporte à AIDL, consulte também FMQ com AIDL.

A infraestrutura de chamada de procedimento remoto (RPC) do HIDL usa mecanismos Binder, ou seja, chamadas envolvem overhead, exigem operações de kernel e podem acionar ação do programador. No entanto, quando os dados precisam ser transferidos entre processos com menos overhead e nenhum envolvimento do kernel, o Fast Message Queue (FMQ, na sigla em inglês) é usado.

O FMQ cria filas de mensagens com as propriedades desejadas. Um Os objetos MQDescriptorSync ou MQDescriptorUnsync podem ser enviado por uma chamada RPC HIDL e usado pelo processo de recebimento para acessar o fila de mensagens.

As filas rápidas de mensagens são compatíveis apenas com C++ e dispositivos com Android 8.0 ou mais recente.

Tipos de MessageQueue

O Android é compatível com dois tipos de fila, conhecidos como variações:

  • Filas não sincronizadas podem ultrapassar o limite e podem ter vários leitores; cada leitor precisa ler os dados a tempo ou perdê-los.
  • Filas sincronizadas não podem entrar em excesso e só podem ter um leitor.

Os dois tipos de fila não podem sofrer underflow (leitura de uma fila vazia) falhar) e só pode 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 a posição de leitura independente dele.

As gravações na fila sempre têm êxito (não são verificadas quanto à sobrecarga), desde que não sejam maiores do que a capacidade de fila configurada (gravações maiores que a a capacidade da fila falha imediatamente). Como cada leitor pode ter uma leitura diferente em vez de esperar que cada leitor leia todos os dados, dados tem permissão para sair da fila sempre que novas gravações precisarem de espaço.

Os leitores são responsáveis por recuperar os dados antes que eles sejam descartados da fila. Leitura que tenta ler mais dados do que os disponíveis falhar imediatamente (se não houver bloqueio) ou esperar que dados suficientes estejam disponíveis (se bloqueio). uma leitura que tenta ler mais dados do que a capacidade da fila sempre vai falhar imediatamente.

Se um leitor não acompanhar o gravador, de modo que a quantidade de dados gravados e ainda não lidos por esse leitor for maior do que a capacidade da fila, o a próxima leitura não retorne dados; isso redefine o processo de leitura para se igualar à posição de gravação mais recente, então retorna uma falha. Se o dados disponíveis para leitura são verificados após o estouro, mas antes da próxima leitura, mostra mais dados disponíveis para leitura do que a capacidade da fila, indicando ocorreu um estouro. (Se a fila sobrecarregar entre a verificação dos dados disponíveis e tentar ler esses dados, a única indicação de estouro é que o leitura falhar.)

Os leitores de uma fila não sincronizada provavelmente não querem redefinir os ponteiros de leitura e gravação da fila. Portanto, ao criar a fila a partir do os leitores do descritor precisam usar um argumento "false" para "resetPointers" .

Sincronizado

Uma fila sincronizada tem um gravador e um leitor com uma única gravação e em uma única posição de leitura. É impossível gravar mais dados do que a fila tem espaço para ou ler mais dados do que a fila retém atualmente. Dependendo da função de gravação ou leitura, que bloqueia ou não, chamado, tenta exceder o espaço disponível ou os dados retornam falhas imediatamente ou bloquear até que a operação desejada possa ser concluída. Tentativas de ler ou gravar mais dados do que a capacidade da fila sempre falhará imediatamente.

Configurar um FMQ

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

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 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 compatível com a funcionalidade da 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 ser kSynchronizedReadWrite para um fila sincronizada ou kUnsynchronizedWrite para um evento fila.
  • uint16_t (neste exemplo) pode ser qualquer tipo definido por HIDL que não envolve buffers aninhados (sem string ou vec identificadores, 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 uma Objeto MQDescriptor recebido do primeiro lado. A O objeto MQDescriptor é enviado por uma chamada RPC HIDL ou AIDL para o processo que contém a segunda extremidade da fila de mensagens. A 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 da sinalização de evento (se a fila estiver bloqueando).
  • Tipo de objeto (<T, flavor>), que inclui o tipo definido por HIDL de elementos de fila e a variação de fila (sincronizados ou não sincronizados).

O objeto MQDescriptor pode ser usado para criar um Objeto MessageQueue:

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

O parâmetro resetPointers indica se a leitura será redefinida e gravar posições como 0 ao criar esse objeto MessageQueue. Em uma fila não sincronizada, a posição de leitura (que é local a cada objeto MessageQueue em filas não sincronizadas) é sempre definido como 0 durante a criação. Normalmente, o MQDescriptor é inicializado criação do primeiro objeto da fila de mensagens. Para ter mais controle sobre os recursos de memória, é possível configurar MQDescriptor manualmente (MQDescriptor é definido em system/libhidl/base/include/hidl/MQDescriptor.h). Depois, crie cada objeto MessageQueue conforme descrito nesta seção.

Bloquear filas e sinalizações de eventos

Por padrão, as filas não oferecem suporte a leituras/gravações de bloqueio. Há dois tipos de chamadas de leitura/gravação de bloqueio:

  • Formato curto, com três parâmetros (ponteiro de dados, número de itens, tempo limite). Suporta o bloqueio em operações individuais de leitura/gravação em uma única fila. Ao usar esse formulário, a fila lida com a flag de evento e os bitmasks. internamente, e o primeiro objeto da fila de mensagens precisa ser inicializada com um segundo parâmetro de true. Por exemplo:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Formato longo, com seis parâmetros (inclui sinalização de evento e bitmasks). Aceita o uso de um objeto EventFlag compartilhado entre várias filas e permite especificar as bitmask de notificação a serem usadas. Nesse caso, o a flag de evento e bitmasks devem ser fornecidos para cada chamada de leitura e gravação.

Para o formato longo, o EventFlag pode ser fornecido explicitamente em cada chamada de readBlocking() e writeBlocking(). Um de as filas podem ser inicializadas com um sinalizador de evento interno, que deve ser extraídos dos objetos MessageQueue dessa fila usando getEventFlagWord() e usada para criar EventFlag em cada processo para uso com outros FMQs. Por outro lado, Os objetos EventFlag podem ser inicializados com qualquer memória.

Em geral, cada fila deve usar apenas um de vídeo ou de formato longo. Não é um erro combiná-los, mas tenha cuidado programação é necessária para obter o resultado desejado.

Marcar a recordação como somente leitura

Por padrão, a memória compartilhada tem permissões de leitura e gravação. Para dessincronizados filas (kUnsynchronizedWrite), talvez o gravador queira remover as permissões de gravação de todas dos leitores antes de distribuir os objetos MQDescriptorUnsync. Isso garante que as outras os processos não podem gravar na fila, o que é recomendado para se proteger contra bugs ou mau comportamento em os processos do leitor. Se o gravador deseja que os leitores possam redefinir a fila sempre que usarem a 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 já existe usuários existentes 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 arquivo MQDescriptor. e região definida como somente leitura (PROT_READ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Leitor: cria fila de mensagens com resetPointer=false (o o padrão é true):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Usar o 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);

availableToWrite() e availableToRead() podem ser usados para determinar quantos dados podem ser transferidos em uma única operação. Em um fila dessincronizada:

  • 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 ultrapassar o limite. isso pode fazer com que availableToRead() retorne um valor maior que o tamanho da fila. A primeira leitura após um estouro falhar e resultar em a posição de leitura desse leitor sendo definida igual ao ponteiro de gravação atual, se o estouro foi informado ou não por meio de availableToRead():

Os métodos read() e write() retornam true se todos os dados solicitados puderem ser (e foram) transferidos de/para o da fila. Esses métodos não bloqueiam. eles terão sucesso (e retornarão true) ou retornar falha (false) imediatamente.

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

As operações de bloqueio são implementadas usando uma palavra de sinalização de evento. Por padrão, cada fila cria e usa sua própria palavra de sinalização para dar suporte à forma abreviada de readBlocking() e writeBlocking(). É possível que múltiplas filas para compartilhar uma única palavra, de modo que um processo possa aguardar gravações ou as leituras em qualquer uma das filas. Um ponteiro para uma palavra de sinalização de evento de uma fila pode ser obtido chamando getEventFlagWord(), e esse ponteiro (ou qualquer para um local adequado de memória compartilhada) pode ser usada para criar um objeto EventFlag a ser transmitido para a forma longa de readBlocking() e writeBlocking() para uma fila. readNotification e writeNotification informam quais bits na flag de evento devem ser usados para sinalizar leituras e gravações nessa fila. readNotification e writeNotification são bitmasks de 32 bits.

readBlocking() aguarda os bits writeNotification; Se esse parâmetro for 0, a chamada sempre falhará. Se o 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 significa que a chamada writeBlocking() correspondente nunca desperta, a menos que o bit esteja configurado em outro lugar. Em uma fila dessincronizada, O método writeBlocking() não espera. Ele ainda deve ser usado para definir o bit de notificação de gravação), e é apropriado que as leituras não definam nenhum bits de notificação. Da mesma forma, writeblocking() falhará se readNotification é 0, e uma gravação bem-sucedida define o valor writeNotification bits.

Para aguardar em várias filas de uma vez, use o método EventFlag Método wait() para aguardar um bitmask de notificações. A O método wait() retorna uma palavra de status com os bits que causaram a configurado para acordar. Essas informações são usadas para verificar se a fila correspondente foi espaço ou dados suficientes para a operação de gravação/leitura desejada e execute write()/read() sem bloqueio. Para receber uma operação de postagem use outra chamada para o método EventFlag wake(). Para uma definição de EventFlag de abstração, consulte system/libfmq/include/fmq/EventFlag.h.

Nenhuma operação de cópia

A write/read/readBlocking/writeBlocking() As APIs levam um ponteiro para um buffer de entrada/saída como argumento e usam memcpy() chama internamente para copiar dados entre o mesmo e o Buffer de anel do FMQ. Para melhorar o desempenho, o Android 8.0 e versões mais recentes incluem um conjunto de APIs que fornecem acesso direto de ponteiro ao buffer de anel, eliminando a precisa usar chamadas memcpy.

Use as seguintes APIs públicas para operações 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 base no anel do FMQ tempo extra. Depois que os dados forem gravados, confirme-os usando commitWrite(). Os métodos beginRead/commitRead funcionam da mesma forma.
  • Os métodos beginRead/Write recebem como entrada o número de mensagens a serem lidas/gravadas e retornam um booleano indicando se o leitura/gravação é possível. Se a leitura ou gravação for possível, o memTx struct é preenchido com ponteiros base que podem ser usados para ponteiro direto acesso à memória compartilhada do buffer de anel.
  • O struct MemRegion contém detalhes sobre um bloco de memória. incluindo o ponteiro base (endereço de base do bloco de memória) e o comprimento em termos de T (comprimento do bloco de memória em termos do espaço da fila de mensagens).
  • O struct MemTransaction contém dois MemRegion. structs, first e second como uma leitura ou gravação o buffer de anel pode exigir um encapsulamento ao início da fila. Isso significa que são necessários dois ponteiros base para ler/gravar dados no FMQ buffer de anel

Para descobrir o endereço base e o comprimento de um 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 conseguir referências ao primeiro e ao segundo MemRegions em uma 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 métodos auxiliares abaixo também fazem parte de MemTransaction:

  • T* getSlot(size_t idx);
    Retorna um ponteiro para o slot idx na MemRegions que fazem parte desta MemTransaction objeto. Se o objeto MemTransaction estiver representando a memória regiões para ler/gravar N itens do tipo T, então o intervalo válido de idx está entre 0 e N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Grave itens nMessages do tipo T nas regiões de memória. descrito pelo objeto, começando pelo índice startIdx. Esse método usa memcpy() e não foi criado para cópia zero operação Se o objeto MemTransaction representar a memória para ler/gravar N itens do tipo T, então o intervalo válido de idx será 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 do regiões de memória descritas pelo objeto começando com startIdx. Isso usa memcpy() e não foi feito para ser usado para cópia zero operação

Enviar a fila por HIDL

No lado da criação:

  1. Crie o objeto da fila de mensagens conforme descrito acima.
  2. Verifique se o objeto é válido com isValid().
  3. Se você estiver aguardando em várias filas passando um EventFlag na forma longa de readBlocking()/writeBlocking(), é possível extrair o ponteiro de sinalização de evento (usando getEventFlagWord()) de um objeto MessageQueue inicializado para criar a sinalização; e use essa flag para criar o objeto EventFlag necessário.
  4. Use o método getDesc() MessageQueue para receber uma objeto descritor.
  5. No arquivo .hal, forneça ao método um parâmetro do tipo fmq_sync ou fmq_unsync, em que T é um definido pelo HIDL mais adequado. Use este campo para enviar o objeto retornado por getDesc() ao processo de recebimento.

No lado do destinatário:

  1. Use o objeto descritor para criar um objeto MessageQueue. Tenha use a mesma variação de fila e o mesmo tipo de dados, ou o modelo falhará compilar.
  2. Se você extraiu uma sinalização de evento, extraia-a do MessageQueue no processo de recebimento.
  3. Use o objeto MessageQueue para transferir dados.