Cola de mensajes rápida (FMQ)

Si busca soporte AIDL, consulte también FMQ con AIDL .

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 gastos generales y sin participación del núcleo, se utiliza el sistema Fast Message Queue (FMQ).

FMQ crea colas de mensajes con las propiedades deseadas. Se puede enviar un objeto MQDescriptorSync o MQDescriptorUnsync a través de una llamada HIDL RPC y el proceso de recepción lo puede utilizar para acceder a la cola de mensajes.

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

Tipos de cola de mensajes

Android admite dos tipos de colas (conocidas 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 pueden tener un escritor.

No sincronizado

Una cola no sincronizada tiene un solo 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 comprueba 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 cada dato, se permite que los datos salgan de la cola cada vez que nuevas escrituras necesiten espacio.

Los lectores son responsables de recuperar los datos antes de que caigan del final de la cola. Una lectura que intenta leer más datos de los disponibles falla inmediatamente (si no es bloqueo) o espera a que haya suficientes datos disponibles (si es bloqueo). Una lectura que intenta leer más datos que la capacidad de la cola siempre falla inmediatamente.

Si un lector no logra seguir el ritmo del 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 igualar 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 siguiente 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 leerlos, la única indicación de desbordamiento es que la lectura falla).

Es probable que los lectores de una cola no sincronizada no quieran restablecer los punteros de lectura y escritura de la cola. Por lo tanto, al crear la cola a partir del descriptor, los lectores deben usar un argumento "falso" para el parámetro "resetPointers".

sincronizado

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

Configurar una FMQ

Una cola de mensajes requiere varios objetos MessageQueue : uno para escribir y uno o más para leer. No existe una configuración explícita de qué objeto se utiliza para escribir o leer; Depende del usuario asegurarse de que no se utilice ningún objeto para lectura y escritura, que haya como máximo un escritor y, para 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 inicializador 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 cola de mensajes con bloqueo.
  • flavor puede ser kSynchronizedReadWrite para una cola sincronizada o kUnsynchronizedWrite para una cola no sincronizada.
  • uint16_t (en este ejemplo) puede ser cualquier tipo definido por HIDL que no involucre buffers anidados (sin tipos 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 RPC HIDL o AIDL 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 asignar el búfer y escribir el puntero.
  • Información para asignar el puntero de lectura (si la cola está sincronizada).
  • Información para asignar la palabra del indicador del 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 (sincronizada o no sincronizada).

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 colas no sincronizadas) siempre se establece en 0 durante la creación. Normalmente, el MQDescriptor se inicializa durante la creación del primer objeto de cola de mensajes. Para obtener 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 e indicadores 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 corta , con tres parámetros (puntero de datos, número de elementos, tiempo de espera). Admite el bloqueo de operaciones de lectura/escritura individuales en una sola cola. Cuando se utiliza este formulario, la cola manejará internamente el indicador de evento y las máscaras de bits, 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 */);
    
  • Formato largo , con seis parámetros (incluye indicador de evento y máscaras de bits). Admite el uso de un objeto EventFlag compartido entre varias colas y permite especificar las máscaras de bits de notificación que se utilizarán. En este caso, se deben proporcionar el indicador de evento y las máscaras de bits a cada llamada de lectura y escritura.

Para el formato largo, EventFlag se puede proporcionar explícitamente en cada llamada readBlocking() y writeBlocking() . Una de las colas puede inicializarse con un indicador de evento interno, que luego debe extraerse de los objetos MessageQueue de esa cola usando getEventFlagWord() y usarse para crear objetos EventFlag en cada proceso para usar con otras FMQ. Alternativamente, los objetos EventFlag se pueden inicializar con cualquier memoria compartida adecuada.

En general, cada cola debe usar solo uno de los siguientes: sin bloqueo, bloqueo de formato corto o bloqueo de formato largo. No es un error mezclarlos, pero se requiere una programación cuidadosa para obtener el resultado deseado.

Marcar la memoria como solo lectura

De forma predeterminada, la memoria compartida tiene permisos de lectura y escritura. Para colas no sincronizadas ( kUnsynchronizedWrite ), es posible que el escritor desee eliminar los permisos de escritura para todos los lectores antes de entregar los objetos MQDescriptorUnsync . Esto garantiza que los otros procesos no puedan escribir en la cola, lo cual se recomienda para proteger contra errores o mal comportamiento en los procesos lectores. Si el escritor quiere que los lectores puedan restablecer la cola cada vez que utilicen MQDescriptorUnsync para crear el lado de lectura de la cola, entonces la memoria no se puede marcar como de solo lectura. Este es el comportamiento predeterminado del constructor `MessageQueue`. Entonces, si ya hay usuarios de esta cola, es necesario cambiar su código para construir la cola con resetPointer=false .

  • Escritor: llame ashmem_set_prot_region con un descriptor de archivo MQDescriptor y una región configurada como de solo lectura ( PROT_READ ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Lector: cree una cola de mensajes con resetPointer=false (el valor predeterminado es true ):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Usando la cola de mensajes

La API pública del objeto MessageQueue es:

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 utilizar 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 provocar 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 de ese lector se establezca igual al puntero de escritura actual, independientemente de si el desbordamiento se informó o no a través de availableToRead() .

Los métodos read() y write() devuelven true si todos los datos solicitados pudieron (y fueron) transferidos hacia/desde la cola. Estos métodos no bloquean; tienen éxito (y devuelven true ) o devuelven un error ( false ) inmediatamente.

Los métodos readBlocking() y writeBlocking() esperan hasta que se pueda completar la operación solicitada, o hasta que se agote 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 utilizando una palabra de indicador de evento. De forma predeterminada, cada cola crea y usa su propia palabra indicadora para admitir la forma corta 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 del indicador de evento 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 pasarlo 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 deben usarse para señalar lecturas y escrituras en esa cola. readNotification y writeNotification son máscaras de bits de 32 bits.

readBlocking() espera 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 esté configurado en otro lugar. En una cola no sincronizada, writeBlocking() no esperará (aún debe usarse para configurar 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 una máscara de bits de notificaciones. El método wait() devuelve una palabra de estado con los bits que provocaron el despertar configurados. Luego, esta información se utiliza para verificar que la cola correspondiente tenga suficiente espacio o datos para la operación de escritura/lectura deseada y realizar una write() / read() sin bloqueo. Para recibir 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 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 directo al puntero al búfer en anillo, 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 al búfer de anillo FMQ. Una vez escritos los datos, confírmelos 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 valor 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 en anillo.
  • 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 de cola de mensajes definido por HIDL).
  • La estructura MemTransaction contiene dos estructuras MemRegion , first y second ya que una lectura o escritura en el búfer circular puede requerir un ajuste al principio 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 a la primera y segunda 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 FMQ utilizando 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 al idx slot dentro de las 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 nMessages de tipo T en las regiones de memoria descritas por el objeto, comenzando desde el índice startIdx . Este método utiliza memcpy() y no debe utilizarse 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 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á diseñado para usarse en una operación de copia cero.

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

Del lado creador:

  1. Cree un objeto de cola de mensajes como se describe anteriormente.
  2. Verifique que el objeto sea válido con isValid() .
  3. Si va a esperar en varias colas pasando un EventFlag a la forma larga de readBlocking() / writeBlocking() , puede extraer el puntero del indicador del 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 método MessageQueue getDesc() 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. Úselo para enviar el objeto devuelto por getDesc() al proceso de recepción.

Del lado receptor:

  1. Utilice el objeto descriptor para crear un objeto MessageQueue . Asegúrese de utilizar el mismo tipo de cola y tipo de datos, o la plantilla no se podrá 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.