File d'attente de messages rapide (FMQ)

Si vous recherchez le support AIDL, consultez également FMQ avec AIDL .

L'infrastructure d'appel de procédure à distance (RPC) de HIDL utilise les mécanismes Binder, ce qui signifie que les appels impliquent une surcharge, nécessitent des opérations du noyau et peuvent déclencher une action du planificateur. Cependant, dans les cas où les données doivent être transférées entre des processus avec moins de surcharge et sans implication du noyau, le système Fast Message Queue (FMQ) est utilisé.

FMQ crée des files d'attente de messages avec les propriétés souhaitées. Un objet MQDescriptorSync ou MQDescriptorUnsync peut être envoyé via un appel HIDL RPC et utilisé par le processus de réception pour accéder à la file d'attente des messages.

Les files d'attente de messages rapides sont prises en charge uniquement en C++ et sur les appareils exécutant Android 8.0 et versions ultérieures.

Types de file d'attente de messages

Android prend en charge deux types de files d'attente (appelés saveurs ) :

  • Les files d'attente non synchronisées peuvent déborder et peuvent avoir de nombreux lecteurs ; chaque lecteur doit lire les données à temps ou les perdre.
  • Les files d'attente synchronisées ne peuvent pas déborder et ne peuvent avoir qu'un seul lecteur.

Les deux types de file d’attente ne sont pas autorisés à déborder (la lecture à partir d’une file d’attente vide échouera) et ne peuvent avoir qu’un seul rédacteur.

Non synchronisé

Une file d'attente non synchronisée n'a qu'un seul rédacteur, mais peut avoir n'importe quel nombre de lecteurs. Il existe une position d'écriture pour la file d'attente ; cependant, chaque lecteur garde une trace de sa propre position de lecture indépendante.

Les écritures dans la file d'attente réussissent toujours (les débordements ne sont pas vérifiés) tant qu'elles ne dépassent pas la capacité de la file d'attente configurée (les écritures supérieures à la capacité de la file d'attente échouent immédiatement). Comme chaque lecteur peut avoir une position de lecture différente, plutôt que d'attendre que chaque lecteur lise chaque élément de données, les données sont autorisées à sortir de la file d'attente chaque fois que de nouvelles écritures ont besoin d'espace.

Les lecteurs sont responsables de récupérer les données avant qu’elles ne tombent en fin de file d’attente. Une lecture qui tente de lire plus de données que ce qui est disponible échoue immédiatement (si elle n'est pas bloquante) ou attend que suffisamment de données soient disponibles (si elle est bloquante). Une lecture qui tente de lire plus de données que la capacité de la file d'attente échoue toujours immédiatement.

Si un lecteur ne parvient pas à suivre le rythme de l'écrivain, de sorte que la quantité de données écrites et non encore lues par ce lecteur est supérieure à la capacité de la file d'attente, la lecture suivante ne renvoie pas de données ; au lieu de cela, il réinitialise la position de lecture du lecteur pour qu'elle soit égale à la dernière position d'écriture, puis renvoie un échec. Si les données disponibles en lecture sont vérifiées après un débordement mais avant la lecture suivante, elles indiquent plus de données disponibles en lecture que la capacité de la file d'attente, indiquant qu'un débordement s'est produit. (Si la file d'attente déborde entre la vérification des données disponibles et la tentative de lecture de ces données, la seule indication de débordement est que la lecture échoue.)

Les lecteurs d'une file d'attente non synchronisée ne souhaitent probablement pas réinitialiser les pointeurs de lecture et d'écriture de la file d'attente. Ainsi, lors de la création de la file d'attente à partir du descripteur, les lecteurs doivent utiliser un argument « faux » pour le paramètre « resetPointers ».

Synchronisé

Une file d'attente synchronisée comporte un graveur et un lecteur avec une seule position d'écriture et une seule position de lecture. Il est impossible d’écrire plus de données que la file d’attente ne dispose d’espace ou de lire plus de données que la file d’attente n’en contient actuellement. Selon que la fonction d'écriture ou de lecture bloquante ou non bloquante est appelée, les tentatives de dépassement de l'espace ou des données disponibles renvoient immédiatement un échec ou se bloquent jusqu'à ce que l'opération souhaitée puisse être terminée. Les tentatives de lecture ou d'écriture de plus de données que la capacité de la file d'attente échoueront toujours immédiatement.

Mettre en place une FMQ

Une file d'attente de messages nécessite plusieurs objets MessageQueue : un pour l'écriture et un ou plusieurs pour la lecture. Il n'y a pas de configuration explicite quant à l'objet utilisé pour l'écriture ou la lecture ; il appartient à l'utilisateur de s'assurer qu'aucun objet n'est utilisé à la fois en lecture et en écriture, qu'il y a au plus un rédacteur et, pour les files d'attente synchronisées, qu'il y a au plus un lecteur.

Création du premier objet MessageQueue

Une file d'attente de messages est créée et configurée avec un seul appel :

#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 */);
  • L'initialiseur MessageQueue<T, flavor>(numElements) crée et initialise un objet qui prend en charge la fonctionnalité de file d'attente de messages.
  • L' MessageQueue<T, flavor>(numElements, configureEventFlagWord) crée et initialise un objet qui prend en charge la fonctionnalité de file d'attente de messages avec blocage.
  • flavor peut être soit kSynchronizedReadWrite pour une file d'attente synchronisée, soit kUnsynchronizedWrite pour une file d'attente non synchronisée.
  • uint16_t (dans cet exemple) peut être n'importe quel type défini par HIDL qui n'implique pas de tampons imbriqués (pas de types string ou vec ), de handles ou d'interfaces.
  • kNumElementsInQueue indique la taille de la file d'attente en nombre d'entrées ; il détermine la taille du tampon de mémoire partagée qui sera allouée à la file d'attente.

Création du deuxième objet MessageQueue

Le deuxième côté de la file d'attente de messages est créé à l'aide d'un objet MQDescriptor obtenu du premier côté. L'objet MQDescriptor est envoyé via un appel RPC HIDL ou AIDL au processus qui contiendra la deuxième extrémité de la file d'attente des messages. Le MQDescriptor contient des informations sur la file d'attente, notamment :

  • Informations pour mapper le tampon et le pointeur d’écriture.
  • Informations pour mapper le pointeur de lecture (si la file d'attente est synchronisée).
  • Informations pour mapper le mot d'indicateur d'événement (si la file d'attente est bloquante).
  • Type d'objet ( <T, flavor> ), qui inclut le type d'éléments de file d'attente défini par HIDL et la version de file d'attente (synchronisée ou non synchronisée).

L'objet MQDescriptor peut être utilisé pour construire un objet MessageQueue :

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

Le paramètre resetPointers indique s'il faut réinitialiser les positions de lecture et d'écriture à 0 lors de la création de cet objet MessageQueue . Dans une file d'attente non synchronisée, la position de lecture (qui est locale à chaque objet MessageQueue dans les files d'attente non synchronisées) est toujours définie sur 0 lors de la création. En règle générale, le MQDescriptor est initialisé lors de la création du premier objet de file d'attente de messages. Pour un contrôle supplémentaire sur la mémoire partagée, vous pouvez configurer manuellement le MQDescriptor ( MQDescriptor est défini dans system/libhidl/base/include/hidl/MQDescriptor.h ), puis créer chaque objet MessageQueue comme décrit dans cette section.

Blocage des files d'attente et des indicateurs d'événements

Par défaut, les files d'attente ne prennent pas en charge le blocage des lectures/écritures. Il existe deux types de blocage des appels en lecture/écriture :

  • Forme courte , avec trois paramètres (pointeur de données, nombre d'éléments, délai d'attente). Prend en charge le blocage des opérations de lecture/écriture individuelles sur une seule file d'attente. Lors de l'utilisation de ce formulaire, la file d'attente gérera l'indicateur d'événement et les masques de bits en interne, et le premier objet de file d'attente de messages doit être initialisé avec un deuxième paramètre true . Par exemple :
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Forme longue , avec six paramètres (comprend un indicateur d'événement et des masques de bits). Prend en charge l'utilisation d'un objet EventFlag partagé entre plusieurs files d'attente et permet de spécifier les masques de bits de notification à utiliser. Dans ce cas, l'indicateur d'événement et les masques de bits doivent être fournis à chaque appel de lecture et d'écriture.

Pour la forme longue, EventFlag peut être fourni explicitement dans chaque appel readBlocking() et writeBlocking() . L'une des files d'attente peut être initialisée avec un indicateur d'événement interne, qui doit ensuite être extrait des objets MessageQueue de cette file d'attente à l'aide de getEventFlagWord() et utilisé pour créer des objets EventFlag dans chaque processus à utiliser avec d'autres FMQ. Alternativement, les objets EventFlag peuvent être initialisés avec n'importe quelle mémoire partagée appropriée.

En général, chaque file d'attente ne doit utiliser qu'un seul type de blocage non bloquant, de blocage court ou de blocage long. Ce n’est pas une erreur de les mélanger, mais une programmation minutieuse est nécessaire pour obtenir le résultat souhaité.

Marquage de la mémoire en lecture seule

Par défaut, la mémoire partagée dispose d'autorisations de lecture et d'écriture. Pour les files d'attente non synchronisées ( kUnsynchronizedWrite ), l'enregistreur souhaitera peut-être supprimer les autorisations d'écriture pour tous les lecteurs avant de distribuer les objets MQDescriptorUnsync . Cela garantit que les autres processus ne peuvent pas écrire dans la file d'attente, ce qui est recommandé pour se protéger contre les bogues ou les mauvais comportements des processus de lecture. Si l'auteur souhaite que les lecteurs puissent réinitialiser la file d'attente chaque fois qu'ils utilisent MQDescriptorUnsync pour créer le côté lecture de la file d'attente, la mémoire ne peut pas être marquée en lecture seule. C'est le comportement par défaut du constructeur `MessageQueue`. Ainsi, s'il existe déjà des utilisateurs de cette file d'attente, leur code doit être modifié pour construire la file d'attente avec resetPointer=false .

  • Writer : appelez ashmem_set_prot_region avec un descripteur de fichier MQDescriptor et une région définie en lecture seule ( PROT_READ ) :
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Reader : créez une file d'attente de messages avec resetPointer=false (la valeur par défaut est true ) :
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Utilisation de MessageQueue

L'API publique de l'objet MessageQueue est :

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() et availableToRead() peuvent être utilisés pour déterminer la quantité de données pouvant être transférées en une seule opération. Dans une file d'attente non synchronisée :

  • availableToWrite() renvoie toujours la capacité de la file d'attente.
  • Chaque lecteur a sa propre position de lecture et effectue son propre calcul pour availableToRead() .
  • Du point de vue d'un lecteur lent, la file d'attente risque de déborder ; cela peut amener availableToRead() à renvoyer une valeur supérieure à la taille de la file d'attente. La première lecture après un débordement échouera et entraînera que la position de lecture de ce lecteur soit égale au pointeur d'écriture actuel, que le débordement ait été signalé ou non via availableToRead() .

Les méthodes read() et write() renvoient true si toutes les données demandées ont pu être (et ont été) transférées vers/depuis la file d'attente. Ces méthodes ne bloquent pas ; Soit ils réussissent (et renvoient true ), soit ils renvoient immédiatement un échec ( false ).

Les méthodes readBlocking() et writeBlocking() attendent que l'opération demandée puisse être terminée ou jusqu'à ce qu'elles expirent (une valeur timeOutNanos de 0 signifie qu'il n'y a jamais d'expiration).

Les opérations de blocage sont mises en œuvre à l'aide d'un mot indicateur d'événement. Par défaut, chaque file d'attente crée et utilise son propre mot indicateur pour prendre en charge la forme courte de readBlocking() et writeBlocking() . Il est possible que plusieurs files d'attente partagent un seul mot, de sorte qu'un processus puisse attendre des écritures ou des lectures dans n'importe laquelle des files d'attente. Un pointeur vers le mot indicateur d'événement d'une file d'attente peut être obtenu en appelant getEventFlagWord() , et ce pointeur (ou tout pointeur vers un emplacement de mémoire partagée approprié) peut être utilisé pour créer un objet EventFlag à passer sous la forme longue de readBlocking() et writeBlocking() pour une file d'attente différente. Les paramètres readNotification et writeNotification indiquent quels bits de l'indicateur d'événement doivent être utilisés pour signaler les lectures et les écritures dans cette file d'attente. readNotification et writeNotification sont des masques de bits 32 bits.

readBlocking() attend les bits writeNotification ; si ce paramètre est 0, l'appel échoue toujours. Si la valeur readNotification est 0, l’appel n’échouera pas, mais une lecture réussie ne définira aucun bit de notification. Dans une file d'attente synchronisée, cela signifierait que l'appel writeBlocking() correspondant ne se réveillera jamais à moins que le bit ne soit défini ailleurs. Dans une file d'attente non synchronisée, writeBlocking() n'attendra pas (il doit toujours être utilisé pour définir le bit de notification d'écriture), et il est approprié que les lectures ne définissent aucun bit de notification. De même, writeblocking() échouera si readNotification vaut 0 et qu'une écriture réussie définit les bits writeNotification spécifiés.

Pour attendre plusieurs files d'attente à la fois, utilisez la méthode wait() d'un objet EventFlag pour attendre un masque de notifications. La méthode wait() renvoie un mot d'état avec les bits qui ont provoqué le réveil. Ces informations sont ensuite utilisées pour vérifier que la file d'attente correspondante dispose de suffisamment d'espace ou de données pour l'opération d'écriture/lecture souhaitée et effectuer une écriture non bloquante write() / read() . Pour obtenir une notification post-opération, utilisez un autre appel à la méthode wake() de EventFlag . Pour une définition de l'abstraction EventFlag , reportez-vous à system/libfmq/include/fmq/EventFlag.h .

Opérations zéro copie

Les API read / write / readBlocking / writeBlocking() prennent un pointeur vers un tampon d'entrée/sortie comme argument et utilisent les appels memcpy() en interne pour copier les données entre celui-ci et le tampon en anneau FMQ. Pour améliorer les performances, Android 8.0 et versions ultérieures incluent un ensemble d'API qui fournissent un accès direct au pointeur dans le tampon en anneau, éliminant ainsi le besoin d'utiliser des appels memcpy .

Utilisez les API publiques suivantes pour les opérations FMQ sans copie :

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);
  • La méthode beginWrite fournit des pointeurs de base dans le tampon en anneau FMQ. Une fois les données écrites, validez-les en utilisant commitWrite() . Les méthodes beginRead / commitRead agissent de la même manière.
  • Les méthodes beginRead / Write prennent en entrée le nombre de messages à lire/écrire et renvoient un booléen indiquant si la lecture/écriture est possible. Si la lecture ou l'écriture est possible, la structure memTx est remplie de pointeurs de base qui peuvent être utilisés pour un accès direct du pointeur à la mémoire partagée du tampon en anneau.
  • La structure MemRegion contient des détails sur un bloc de mémoire, y compris le pointeur de base (adresse de base du bloc de mémoire) et la longueur en termes de T (longueur du bloc de mémoire en termes de type de file d'attente de messages défini par HIDL).
  • La structure MemTransaction contient deux structures MemRegion , first et second car une lecture ou une écriture dans le tampon en anneau peut nécessiter un retour au début de la file d'attente. Cela signifierait que deux pointeurs de base sont nécessaires pour lire/écrire des données dans le tampon en anneau FMQ.

Pour obtenir l'adresse de base et la longueur d'une structure 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

Pour obtenir des références aux premier et deuxième MemRegion dans un objet MemTransaction :

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

Exemple d'écriture dans la FMQ à l'aide d'API sans copie :

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
}

Les méthodes d'assistance suivantes font également partie de MemTransaction :

  • T* getSlot(size_t idx);
    Renvoie un pointeur vers l'emplacement idx dans les MemRegions qui font partie de cet objet MemTransaction . Si l'objet MemTransaction représente les régions de mémoire pour lire/écrire N éléments de type T, alors la plage valide d' idx est comprise entre 0 et N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Écrivez les éléments nMessages de type T dans les régions mémoire décrites par l'objet, en commençant par l'index startIdx . Cette méthode utilise memcpy() et n'est pas destinée à être utilisée pour une opération sans copie. Si l'objet MemTransaction représente la mémoire pour lire/écrire N éléments de type T, alors la plage valide d' idx est comprise entre 0 et N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Méthode d'assistance pour lire les éléments nMessages de type T à partir des régions mémoire décrites par l'objet à partir de startIdx . Cette méthode utilise memcpy() et n'est pas destinée à être utilisée pour une opération sans copie.

Envoi de la file d'attente via HIDL

Côté création :

  1. Créez un objet de file d'attente de messages comme décrit ci-dessus.
  2. Vérifiez que l'objet est valide avec isValid() .
  3. Si vous attendez plusieurs files d'attente en passant un EventFlag dans la forme longue de readBlocking() / writeBlocking() , vous pouvez extraire le pointeur d'indicateur d'événement (en utilisant getEventFlagWord() ) à partir d'un objet MessageQueue qui a été initialisé pour créer l'indicateur, et utilisez cet indicateur pour créer l'objet EventFlag nécessaire.
  4. Utilisez la méthode MessageQueue getDesc() pour obtenir un objet descripteur.
  5. Dans le fichier .hal , donnez à la méthode un paramètre de type fmq_sync ou fmq_unsyncT est un type approprié défini par HIDL. Utilisez ceci pour envoyer l'objet renvoyé par getDesc() au processus de réception.

Du côté de la réception :

  1. Utilisez l'objet descripteur pour créer un objet MessageQueue . Assurez-vous d'utiliser la même version de file d'attente et le même type de données, sinon la compilation du modèle échouera.
  2. Si vous avez extrait un indicateur d'événement, extrayez l'indicateur de l'objet MessageQueue correspondant dans le processus de réception.
  3. Utilisez l'objet MessageQueue pour transférer des données.