Cola de mensajes rápidos (FMQ)

La infraestructura de llamada a procedimiento remoto (RPC) de HIDL utiliza mecanismos Binder, lo que significa que las llamadas implican una sobrecarga, requieren operaciones del kernel y pueden desencadenar una acción del programador. Sin embargo, para los casos en los que los datos deben transferirse entre procesos con menos sobrecarga y sin participación del kernel, se utiliza el sistema Fast Message Queue (FMQ).

FMQ crea colas de mensajes con las propiedades deseadas. Un objeto MQDescriptorSync o MQDescriptorUnsync puede enviarse a través de una llamada HIDL RPC y ser utilizado por el proceso de recepción para acceder a la cola de mensajes.

Las colas de mensajes rápidos solo se admiten en C++ y en dispositivos con Android 8.0 y versiones posteriores.

Tipos de MessageQueue

Android admite dos tipos de cola (conocidos como sabores ):

  • Las colas no sincronizadas pueden desbordarse y pueden tener muchos lectores; cada lector debe leer los datos a tiempo o perderlos.
  • Las colas sincronizadas no pueden desbordarse y solo pueden tener un lector.

No se permite el desbordamiento de ambos tipos de cola (la lectura de una cola vacía fallará) y solo puede tener un escritor.

no sincronizado

Una cola no sincronizada tiene solo un escritor, pero puede tener cualquier número de lectores. Hay una posición de escritura para la cola; sin embargo, cada lector realiza un seguimiento de su propia posición de lectura independiente.

Las escrituras en la cola siempre se realizan correctamente (no se comprueban si hay desbordamiento) siempre que no superen la capacidad de la cola configurada (las escrituras superiores a la capacidad de la cola fallan inmediatamente). Como cada lector puede tener una posición de lectura diferente, en lugar de esperar a que cada lector lea todos los datos, se permite que los datos caigan de la cola siempre que las nuevas escrituras necesiten espacio.

Las lecturas son responsables de recuperar datos antes de que caigan al final de la cola. Una lectura que intenta leer más datos de los que están disponibles falla inmediatamente (si no bloquea) o espera a que haya suficientes datos disponibles (si bloquea). Una lectura que intenta leer más datos que la capacidad de la cola siempre falla inmediatamente.

Si un lector no puede mantenerse al día con el escritor, de modo que la cantidad de datos escritos y aún no leídos por ese lector es mayor que la capacidad de la cola, la siguiente lectura no devuelve datos; en su lugar, restablece la posición de lectura del lector para que sea igual a la última posición de escritura y luego devuelve un error. Si los datos disponibles para leer se verifican después del desbordamiento pero antes de la próxima lectura, muestra más datos disponibles para leer que la capacidad de la cola, lo que indica que se ha producido un desbordamiento. (Si la cola se desborda entre la verificación de los datos disponibles y el intento de leer esos datos, la única indicación de desbordamiento es que la lectura falla).

sincronizado

Una cola sincronizada tiene un escritor y un lector con una sola posición de escritura y una sola posición de lectura. Es imposible escribir más datos de los que tiene espacio en la cola o leer más datos de los que contiene actualmente la cola. Dependiendo de si se llama a la función de escritura o lectura con bloqueo o sin bloqueo, los intentos de exceder el espacio disponible o los datos devuelven un error inmediatamente o se bloquean hasta que se puede completar la operación deseada. Los intentos de leer o escribir más datos que la capacidad de la cola siempre fallarán de inmediato.

Configuración de un FMQ

Una cola de mensajes requiere varios objetos MessageQueue : uno para escribir y uno o más para leer. No hay una configuración explícita de qué objeto se usa para escribir o leer; depende del usuario asegurarse de que ningún objeto se utilice tanto para leer como para escribir, que haya como máximo un escritor y, para las colas sincronizadas, que haya como máximo un lector.

Creando el primer objeto MessageQueue

Una cola de mensajes se crea y configura con una sola llamada:

#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 */);
  • El MessageQueue<T, flavor>(numElements) crea e inicializa un objeto que admite la funcionalidad de la cola de mensajes.
  • El MessageQueue<T, flavor>(numElements, configureEventFlagWord) crea e inicializa un objeto que admite la funcionalidad de la cola de mensajes con bloqueo.
  • El tipo puede ser flavor para una cola kUnsynchronizedWrite kSynchronizedReadWrite una cola no sincronizada.
  • uint16_t (en este ejemplo) puede ser cualquier tipo definido por HIDL que no involucre búferes anidados (sin tipos de string o vec ), identificadores o interfaces.
  • kNumElementsInQueue indica el tamaño de la cola en número de entradas; determina el tamaño del búfer de memoria compartida que se asignará a la cola.

Creando el segundo objeto MessageQueue

El segundo lado de la cola de mensajes se crea utilizando un objeto MQDescriptor obtenido del primer lado. El objeto MQDescriptor se envía a través de una llamada HIDL RPC al proceso que contendrá el segundo extremo de la cola de mensajes. El MQDescriptor contiene información sobre la cola, que incluye:

  • Información para mapear el búfer y el puntero de escritura.
  • Información para mapear el puntero de lectura (si la cola está sincronizada).
  • Información para mapear la palabra del indicador de evento (si la cola está bloqueando).
  • Tipo de objeto ( <T, flavor> ), que incluye el tipo de elementos de cola definidos por HIDL y el tipo de cola (sincronizado o no sincronizado).

El objeto MQDescriptor se puede utilizar para construir un objeto MessageQueue :

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

El parámetro resetPointers indica si se deben restablecer las posiciones de lectura y escritura a 0 al crear este objeto MessageQueue . En una cola no sincronizada, la posición de lectura (que es local para cada objeto MessageQueue en las colas no sincronizadas) siempre se establece en 0 durante la creación. Normalmente, MQDescriptor se inicializa durante la creación del primer objeto de cola de mensajes. Para un control adicional sobre la memoria compartida, puede configurar MQDescriptor manualmente ( MQDescriptor se define en system/libhidl/base/include/hidl/MQDescriptor.h ) y luego crear cada objeto MessageQueue como se describe en esta sección.

Bloqueo de colas y banderas de eventos

De forma predeterminada, las colas no admiten el bloqueo de lecturas/escrituras. Hay dos tipos de bloqueo de llamadas de lectura/escritura:

  • Forma abreviada , con tres parámetros (puntero de datos, número de elementos, tiempo de espera). Admite el bloqueo de operaciones individuales de lectura/escritura en una sola cola. Al usar este formulario, la cola manejará el indicador de evento y las máscaras de bits internamente, y el primer objeto de la cola de mensajes debe inicializarse con un segundo parámetro de true . Por ejemplo:
    // 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() y availableToRead() se pueden usar para determinar cuántos datos se pueden transferir en una sola operación. En una cola no sincronizada:

  • availableToWrite() siempre devuelve la capacidad de la cola.
  • Cada lector tiene su propia posición de lectura y realiza su propio cálculo para availableToRead() .
  • Desde el punto de vista de un lector lento, se permite que la cola se desborde; esto puede dar como resultado que availableToRead() devuelva un valor mayor que el tamaño de la cola. La primera lectura después de un desbordamiento fallará y dará como resultado que la posición de lectura para ese lector se establezca igual al puntero de escritura actual, ya sea que el desbordamiento se haya informado o no a través de availableToRead() .

Los métodos read() y write() devuelven true si todos los datos solicitados se pudieron (y se transfirieron) a/desde la cola. Estos métodos no bloquean; tienen éxito (y devuelven true ), o devuelven falla ( false ) inmediatamente.

Los readBlocking() y writeBlocking() esperan hasta que se puede completar la operación solicitada o hasta que se agota el tiempo de espera (un valor de timeOutNanos de 0 significa que nunca se agota el tiempo de espera).

Las operaciones de bloqueo se implementan mediante una palabra indicadora de evento. De forma predeterminada, cada cola crea y usa su propia palabra indicadora para admitir la forma abreviada de readBlocking() y writeBlocking() . Es posible que varias colas compartan una sola palabra, de modo que un proceso pueda esperar escrituras o lecturas en cualquiera de las colas. Se puede obtener un puntero a la palabra indicadora de eventos de una cola llamando a getEventFlagWord() , y ese puntero (o cualquier puntero a una ubicación de memoria compartida adecuada) se puede usar para crear un objeto EventFlag para pasar a la forma larga de readBlocking() y writeBlocking() para una cola diferente. Los parámetros readNotification y writeNotification indican qué bits del indicador de evento se deben usar para indicar lecturas y escrituras en esa cola. readNotification y writeNotification son máscaras de bits de 32 bits.

readBlocking() espera en los bits writeNotification ; si ese parámetro es 0, la llamada siempre falla. Si el valor readNotification es 0, la llamada no fallará, pero una lectura exitosa no establecerá ningún bit de notificación. En una cola sincronizada, esto significaría que la llamada writeBlocking() correspondiente nunca se activará a menos que el bit se establezca en otro lugar. En una cola no sincronizada, writeBlocking() no esperará (todavía debe usarse para establecer el bit de notificación de escritura), y es apropiado que las lecturas no establezcan ningún bit de notificación. De manera similar, writeblocking() fallará si readNotification es 0, y una escritura exitosa establece los bits writeNotification especificados.

Para esperar en varias colas a la vez, use el método wait() de un objeto EventFlag para esperar en una máscara de bits de notificaciones. El método wait() devuelve una palabra de estado con los bits que provocaron el conjunto de activación. Luego, esta información se usa para verificar que la cola correspondiente tenga suficiente espacio o datos para la operación de escritura/lectura deseada y realizar una operación de write() / read() sin bloqueo. Para obtener una notificación posterior a la operación, utilice otra llamada al método wake() de EventFlag . Para obtener una definición de la abstracción EventFlag , consulte system/libfmq/include/fmq/EventFlag.h .

Operaciones de copia cero

Las API de read / write / readBlocking / writeBlocking() toman un puntero a un búfer de entrada/salida como argumento y usan llamadas memcpy() internamente para copiar datos entre el mismo y el búfer de anillo FMQ. Para mejorar el rendimiento, Android 8.0 y versiones posteriores incluyen un conjunto de API que brindan acceso de puntero directo al búfer circular, lo que elimina la necesidad de usar llamadas memcpy .

Utilice las siguientes API públicas para operaciones FMQ de copia cero:

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);
  • El método beginWrite proporciona punteros base en el búfer de anillo de FMQ. Después de escribir los datos, confírmalos usando commitWrite() . Los métodos beginRead / commitRead actúan de la misma manera.
  • Los métodos beginRead / Write toman como entrada el número de mensajes a leer/escribir y devuelven un booleano que indica si la lectura/escritura es posible. Si la lectura o escritura es posible, la estructura memTx se completa con punteros base que se pueden usar para el acceso directo del puntero a la memoria compartida del búfer circular.
  • La estructura MemRegion contiene detalles sobre un bloque de memoria, incluido el puntero base (dirección base del bloque de memoria) y la longitud en términos de T (longitud del bloque de memoria en términos del tipo definido por HIDL de la cola de mensajes).
  • La estructura MemTransaction contiene dos estructuras MemRegion , la first y la second , ya que una lectura o escritura en el búfer circular puede requerir un retorno al comienzo de la cola. Esto significaría que se necesitan dos punteros base para leer/escribir datos en el búfer de anillo FMQ.

Para obtener la dirección base y la longitud de una estructura 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 obtener referencias al primer y segundo MemRegion dentro de un objeto MemTransaction :

const MemRegion& getFirstRegion(); // get a reference to the first MemRegion
const MemRegion& getSecondRegion(); // get a reference to the second MemRegion

Ejemplo de escritura en el FMQ usando API de copia cero:

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
}

Los siguientes métodos auxiliares también forman parte de MemTransaction :

  • T* getSlot(size_t idx);
    Devuelve un puntero a la ranura idx dentro de MemRegions que forman parte de este objeto MemTransaction . Si el objeto MemTransaction representa las regiones de memoria para leer/escribir N elementos de tipo T, entonces el rango válido de idx está entre 0 y N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Escriba elementos de nMessages de tipo T en las regiones de memoria descritas por el objeto, a partir del índice startIdx . Este método usa memcpy() y no debe usarse para una operación de copia cero. Si el objeto MemTransaction representa memoria para leer/escribir N elementos de tipo T, entonces el rango válido de idx está entre 0 y N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Método auxiliar para leer elementos de nMessages de tipo T de las regiones de memoria descritas por el objeto a partir de startIdx . Este método usa memcpy() y no está destinado a ser usado para una operación de copia cero.

Envío de la cola a través de HIDL

En el lado creador:

  1. Cree un objeto de cola de mensajes como se describe anteriormente.
  2. Verifica que el objeto sea válido con isValid() .
  3. Si va a esperar en varias colas al pasar un EventFlag a la forma larga de readBlocking() / writeBlocking() , puede extraer el puntero del indicador de evento (usando getEventFlagWord() ) de un objeto MessageQueue que se inicializó para crear el indicador, y use esa bandera para crear el objeto EventFlag necesario.
  4. Utilice el MessageQueue getDesc() de MessageQueue para obtener un objeto descriptor.
  5. En el archivo .hal , asigne al método un parámetro de tipo fmq_sync o fmq_unsync donde T es un tipo adecuado definido por HIDL. Use esto para enviar el objeto devuelto por getDesc() al proceso de recepción.

En el lado receptor:

  1. Utilice el objeto descriptor para crear un objeto MessageQueue . Asegúrese de usar el mismo tipo de cola y tipo de datos, o la plantilla no se compilará.
  2. Si extrajo un indicador de evento, extraiga el indicador del objeto MessageQueue correspondiente en el proceso de recepción.
  3. Utilice el objeto MessageQueue para transferir datos.