Fila de mensajes rápidos (FMQ)

La infraestructura de llamadas de procedimiento remoto (RPC) de HIDL usa mecanismos de Binder, lo que significa que las llamadas implican sobrecarga, requieren operaciones del kernel y pueden activar la acción del programador. Sin embargo, en los casos en que los datos se deben transferir entre procesos con menos sobrecarga y sin participación del kernel, se usa el sistema de cola de mensajes rápidos (FMQ).

FMQ crea filas de mensajes con las propiedades deseadas. Puedes enviar un objeto MQDescriptorSync o MQDescriptorUnsync a través de una llamada de RPC de HIDL, y el proceso receptor usará el objeto para acceder a la cola de mensajes.

Tipos de colas

Android admite dos tipos de colas (conocidos como variantes):

  • Las colas no sincronizadas pueden desbordarse y 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 subdesbordamiento de ambos tipos de cola (la lectura de una cola vacía falla) y solo puede tener un escritor.

Colas no sincronizadas

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

Las operaciones de escritura en la cola siempre se realizan correctamente (no se verifica si hay desbordamiento) siempre que no sean mayores que la capacidad de la cola configurada (las operaciones de escritura mayores que la capacidad de la cola fallan de inmediato). Como cada lector puede tener una posición de lectura diferente, en lugar de esperar a que cada lector lea cada dato, los datos se quitan de la cola cada vez que las operaciones de escritura nuevas necesitan el espacio.

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

Si un lector no puede seguir el ritmo del escritor, de modo que la cantidad de datos que escribió y que aún no leyó ese lector es mayor que la capacidad de la cola, la siguiente lectura no muestra datos. En su lugar, restablece la posición de lectura del lector para que sea igual a la posición de escritura más reciente y, luego, muestra un error. Si los datos disponibles para leer se verifican después del desbordamiento, pero antes de la siguiente lectura, se muestran más datos disponibles para leer que la capacidad de la cola, lo que indica que se produjo 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 operación de lectura falla).

Colas sincronizadas

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 la cola o leer más datos de los que contiene actualmente. Según si se llama a la función de lectura o escritura con bloqueo o sin bloqueo, los intentos de superar el espacio o los datos disponibles muestran un error de inmediato 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 fallan de inmediato.

Configura una FMQ

Una lista de tareas en cola requiere varios objetos MessageQueue: uno en el que se escribirá y uno o más de los que se leerá. No hay una configuración explícita acerca de qué objeto se usa para escritura o lectura; el usuario es responsable de asegurarse de que no se use ningún objeto para lectura y escritura, que haya como máximo un escritor y, para las colas sincronizadas, que haya como máximo un lector.

Crea el primer objeto MessageQueue

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

Crea el segundo objeto MessageQueue

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

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

Puedes usar el objeto MQDescriptor 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 mientras se crea 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. Por lo general, el MQDescriptor se inicializa durante la creación del primer objeto de cola de mensajes. Para tener más control sobre la memoria compartida, puedes configurar el MQDescriptor de forma manual (MQDescriptor se define en system/libhidl/base/include/hidl/MQDescriptor.h) y, luego, crear cada objeto MessageQueue como se describe en esta sección.

Colas de bloqueo y marcas de eventos

De forma predeterminada, las filas no admiten el bloqueo de operaciones de lectura y escritura. Existen dos tipos de bloqueo de llamadas de lectura y escritura:

  • La forma corta, con tres parámetros (puntero de datos, cantidad de elementos, tiempo de espera), admite el bloqueo en operaciones individuales de lectura y escritura en una sola cola. Cuando se usa este formulario, la cola controla la marca de evento y las máscaras de bits de forma interna, y el primer objeto de cola de mensajes se debe inicializar 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 */);
    
  • El formato largo, con seis parámetros (incluye la marca de evento y las máscaras de bits), admite el uso de un objeto EventFlag compartido entre varias filas y permite especificar las máscaras de bits de notificación que se usarán. En este caso, se deben proporcionar la marca de evento y las máscaras de bits a cada llamada de lectura y escritura.

Para el formato largo, puedes proporcionar el EventFlag de forma explícita en cada llamada a readBlocking() y writeBlocking(). Puedes inicializar una de las filas con una marca de evento interno, que luego se debe extraer de los objetos MessageQueue de esa fila con getEventFlagWord() y usar para crear un objeto EventFlag en cada proceso para usarlo con otras FMQ. Como alternativa, puedes inicializar los objetos EventFlag con cualquier memoria compartida adecuada.

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

Cómo marcar la memoria como de solo lectura

De forma predeterminada, la memoria compartida tiene permisos de lectura y escritura. En el caso de las filas no sincronizadas (kUnsynchronizedWrite), es posible que el escritor desee quitar 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 que se recomienda para proteger contra errores o comportamientos incorrectos en los procesos de lectura. Si el escritor quiere que los lectores puedan restablecer la cola cada vez que usan MQDescriptorUnsync para crear el lado de lectura de la cola, la memoria no se puede marcar como de solo lectura. Este es el comportamiento predeterminado del constructor MessageQueue. Por lo tanto, si hay usuarios existentes de esta cola, se debe cambiar su código para construir la cola con resetPointer=false.

  • Escritor: Llama a 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: Crea una cola de mensajes con resetPointer=false (el valor predeterminado es true):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Cómo usar MessageQueue

La API pública del objeto MessageQueue es la siguiente:

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

Puedes usar availableToWrite() y availableToRead() para determinar cuántos datos se pueden transferir en una sola operación. En una fila sin sincronizar, haz lo siguiente:

  • availableToWrite() siempre muestra 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, la cola puede desbordarse, lo que puede provocar que availableToRead() muestre un valor mayor que el tamaño de la cola. La primera lectura después de un desbordamiento falla y da como resultado que la posición de lectura para ese lector sea igual al puntero de escritura actual, sin importar si el desbordamiento se informó o no a través de availableToRead().

Los métodos read() y write() muestran true si todos los datos solicitados se pudieron transferir (y se transfirieron) desde y hacia la cola. Estos métodos no bloquean; o bien se realizan correctamente (y muestran true) o muestran una falla (false) de inmediato.

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 timeOutNanos de 0 significa que nunca se agotó el tiempo de espera).

Las operaciones de bloqueo se implementan mediante una palabra marca de evento. De forma predeterminada, cada cola crea y usa su propia palabra de marca para admitir la forma corta de readBlocking() y writeBlocking(). Varias colas pueden compartir una sola palabra, por lo que un proceso puede esperar las operaciones de escritura o lectura en cualquiera de las colas. Si llamas a getEventFlagWord(), puedes obtener un puntero a la palabra de marca del evento de una cola y puedes usar ese puntero (o cualquier puntero a una ubicación adecuada de memoria compartida) para crear un objeto EventFlag y pasar a la forma larga de readBlocking() y writeBlocking() para una cola diferente. Los parámetros readNotification y writeNotification indican qué bits de la marca de evento se deben usar para indicar las operaciones de lectura y escritura 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 de readNotification es 0, la llamada no falla, pero una lectura correcta no establecerá ningún bit de notificación. En una cola sincronizada, esto significa que la llamada writeBlocking() correspondiente nunca se activa, a menos que el bit se establezca en otro lugar. En una cola no sincronizada, writeBlocking() no espera (debe usarse para establecer el bit de notificación de escritura) y es apropiado que las operaciones de lectura no establezcan ningún bit de notificación. De manera similar, writeblocking() falla si readNotification es 0, y una operación de escritura correcta establece los bits writeNotification especificados.

Para esperar en varias filas a la vez, usa el método wait() de un objeto EventFlag para esperar en una máscara de bits de notificaciones. El método wait() muestra una palabra de estado con los bits que causaron 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 lectura y escritura deseada y realizar un write() y read() no bloqueador. Para obtener una notificación después de la operación, usa otra llamada al método wake() del objeto EventFlag. Para obtener una definición de la abstracción EventFlag, consulta system/libfmq/include/fmq/EventFlag.h.

Operaciones de copia cero

Los métodos read, write, readBlocking y writeBlocking() toman un puntero a un búfer de entrada y salida como argumento y usan llamadas memcpy() de forma interna para copiar datos entre el mismo y el búfer circular de FMQ. Para mejorar el rendimiento, Android 8.0 y versiones posteriores incluyen un conjunto de APIs que proporcionan acceso directo del puntero al búfer circular, lo que elimina la necesidad de usar llamadas memcpy.

Usa las siguientes APIs públicas para operaciones de FMQ sin copia:

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 de FMQ. Después de escribir los datos, confirma con commitWrite(). Los métodos beginRead y commitRead actúan de la misma manera.
  • Los métodos beginRead y Write toman como entrada la cantidad de mensajes que se deben leer y escribir, y muestran un valor booleano que indica si es posible la lectura o la escritura. Si la lectura o escritura es posible, la estructura memTx se propaga con punteros base que se pueden usar para el acceso directo de punteros a la memoria compartida del búfer circular.
  • La struct MemRegion contiene detalles sobre un bloque de memoria, incluido el puntero de 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 la cola de mensajes definido por HIDL).
  • La estructura MemTransaction contiene dos estructuras MemRegion, first y second, ya que una operación de lectura o escritura en el búfer circular puede requerir un recálculo al principio de la cola. Esto significa que se necesitan dos punteros base para leer y escribir datos en el búfer circular de FMQ.

Para obtener la dirección base y la longitud de una estructura MemRegion, sigue estos pasos:

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 estructuras MemRegion dentro de un objeto MemTransaction, haz lo siguiente:

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

Ejemplo de escritura en la FMQ con APIs 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); muestra un puntero a la ranura idx dentro de MemRegions que forma parte de este objeto MemTransaction. Si el objeto MemTransaction representa las regiones de memoria para leer y escribir N elementos de tipo T, el rango válido de idx está entre 0 y N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1); escribe elementos nMessages de tipo T en las regiones de memoria que describe el objeto, a partir del índice startIdx. Este método usa memcpy() y no está diseñado para una operación de copia cero. Si el objeto MemTransaction representa la memoria para leer y escribir N elementos de tipo T, el rango válido de idx está entre 0 y N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1); es un método auxiliar para leer elementos nMessages de tipo T de las regiones de memoria que describe 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ía la cola a través de HIDL

Del lado de la creación:

  1. Crea un objeto de cola de mensajes como se describió anteriormente.
  2. Verifica que el objeto sea válido con isValid().
  3. Si esperas en varias filas pasando EventFlag al formato largo de readBlocking() o writeBlocking(), puedes extraer el puntero de marca de evento (con getEventFlagWord()) de un objeto MessageQueue que se inicializó para crear la marca y usar esa marca para crear el objeto EventFlag necesario.
  4. Usa el método getDesc() de MessageQueue para obtener un objeto descriptor.
  5. En el archivo HAL, asigna al método un parámetro de tipo fmq_sync o fmq_unsync, donde T es un tipo adecuado definido por HIDL. Úsalo para enviar el objeto que muestra getDesc() al proceso receptor.

En el lado receptor:

  1. Usa el objeto descriptor para crear un objeto MessageQueue. Usa el mismo tipo de datos y tipo de cola, o la plantilla no se compila.
  2. Si extrajiste una marca de evento, extrae la marca del objeto MessageQueue correspondiente en el proceso de recepción.
  3. Usa el objeto MessageQueue para transferir datos.