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 serkSynchronizedReadWrite
para una fila sincronizada okUnsynchronizedWrite
para una fila no sincronizada.uint16_t
(en este ejemplo) puede ser cualquier tipo definido por HIDL que no involucre búferes anidados (sin tiposstring
nivec
), 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 archivoMQDescriptor
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 estrue
):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 deavailableToRead()
.
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 concommitWrite()
. Los métodosbeginRead
ycommitRead
actúan de la misma manera. - Los métodos
beginRead
yWrite
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 estructuramemTx
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 deT
(longitud del bloque de memoria en términos del tipo de la cola de mensajes definido por HIDL). - La estructura
MemTransaction
contiene dos estructurasMemRegion
,first
ysecond
, 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 ranuraidx
dentro deMemRegions
que forma parte de este objetoMemTransaction
. Si el objetoMemTransaction
representa las regiones de memoria para leer y escribir N elementos de tipoT
, el rango válido deidx
está entre 0 y N-1.bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
escribe elementosnMessages
de tipoT
en las regiones de memoria que describe el objeto, a partir del índicestartIdx
. Este método usamemcpy()
y no está diseñado para una operación de copia cero. Si el objetoMemTransaction
representa la memoria para leer y escribir N elementos de tipoT
, el rango válido deidx
está entre 0 y N-1.bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
es un método auxiliar para leer elementosnMessages
de tipoT
de las regiones de memoria que describe el objeto a partir destartIdx
. Este método usamemcpy()
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:
- Crea un objeto de cola de mensajes como se describió anteriormente.
- Verifica que el objeto sea válido con
isValid()
. - Si esperas en varias filas pasando
EventFlag
al formato largo dereadBlocking()
owriteBlocking()
, puedes extraer el puntero de marca de evento (congetEventFlagWord()
) de un objetoMessageQueue
que se inicializó para crear la marca y usar esa marca para crear el objetoEventFlag
necesario. - Usa el método
getDesc()
deMessageQueue
para obtener un objeto descriptor. - En el archivo HAL, asigna al método un parámetro de tipo
fmq_sync
ofmq_unsync
, dondeT
es un tipo adecuado definido por HIDL. Úsalo para enviar el objeto que muestragetDesc()
al proceso receptor.
En el lado receptor:
- 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. - Si extrajiste una marca de evento, extrae la marca del objeto
MessageQueue
correspondiente en el proceso de recepción. - Usa el objeto
MessageQueue
para transferir datos.