File d'attente de messages rapide (FMQ)

Si vous avez besoin d'aide pour AIDL, consultez également FMQ avec AIDL :

L'infrastructure d'appel de procédure à distance (RPC) de HIDL utilise des mécanismes de liaison, Cela signifie que les appels impliquent des frais généraux, nécessitent des opérations de noyau et peuvent déclencher l'action du planificateur. Toutefois, pour les cas où les données doivent être transférées avec moins de frais généraux et sans implication du noyau, le service Fast Message Queue (FMQ) est utilisé.

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

Les files d'attente de messages rapides ne sont compatibles qu'avec C++ et sur les appareils. équipés d'Android 8.0 ou version ultérieure.

Types MessageQueue

Android accepte deux types de files d'attente (appelés flavors):

  • Les files d'attente désynchronisées sont autorisées à déborder et peuvent comporter de nombreuses lecteurs ; chaque lecteur doit lire les données à temps ou les perdre.
  • Les files d'attente synchronisées ne sont pas autorisées à déborder et ne peuvent contenir que un lecteur.

Le dépassement de capacité négatif n'est pas autorisé pour les deux types de files d'attente (lecture à partir d'une file d'attente vide) échoue) et ne peut avoir qu'un seul écrivain.

Désynchronisé

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

Les écritures dans la file d'attente réussissent toujours (sans vérification de dépassement) tant que elles ne dépassent pas la capacité de la file d'attente configurée (les écritures sont supérieures au la capacité de la file d'attente échoue immédiatement). Comme chaque lecteur peut avoir une lecture différente au lieu d'attendre que chaque lecteur lise toutes les données, peut tomber de la file d'attente chaque fois que les nouvelles écritures nécessitent de l'espace.

Les lecteurs sont chargés de récupérer les données avant qu'elles ne finissent de dans la file d'attente. Une lecture qui tente de lire plus de données que disponible échoue immédiatement (s'il s'agit d'une règle non bloquante) ou attend qu'une quantité suffisante de données soit disponible (si le blocage). Lecture qui tente toujours de lire plus de données que la capacité de la file d'attente échoue immédiatement.

Si un lecteur ne parvient pas à suivre le rythme de l'auteur, de sorte que la quantité de données écrit et pas encore lu par le lecteur dépasse la capacité de la file d'attente, la lecture suivante ne renvoie pas de données ; au lieu de cela, il réinitialise le temps de lecture du lecteur pour qu'elle soit égale à la dernière position d'écriture, puis renvoie l'échec. Si le les données disponibles en lecture sont vérifiées après un dépassement, mais avant la lecture suivante, elles affiche plus de données disponibles en lecture que la capacité de la file d'attente, ce qui indique un débordement s'est produit. (Si la file d'attente déborde entre la vérification des données disponibles et que vous tentez de lire ces données, la seule indication de dépassement est que échec de lecture.)

Les lecteurs d'une file d'attente désynchronisée ne veulent probablement pas réinitialiser les pointeurs de lecture et d'écriture de la file d'attente. Lors de la création de la file d'attente les lecteurs de descripteurs doivent utiliser un argument "false" pour "resetPointers" .

Synchronisé

Une file d'attente synchronisée contient un rédacteur et un lecteur avec une seule écriture et une seule position de lecture. Il est impossible d'écrire plus de données que la file d'attente peut lire plus de données que ce qu'elle contient actuellement. Selon que la fonction d'écriture ou de lecture bloquante ou non bloquante est est appelé, les tentatives de dépassement de l'espace disponible ou les données renvoient un échec immédiatement ou le bloquer jusqu'à ce que l'opération souhaitée puisse être effectuée. Tentatives de lire ou écrire plus de données que la capacité de la file d'attente échoue toujours immédiatement.

Configurer un FMQ

Une file d'attente de messages nécessite plusieurs objets MessageQueue: un à et un ou plusieurs éléments à lire. Il n'y a pas de texte la configuration de l'objet utilisé pour l'écriture ou la lecture ; c'est à vous pour s'assurer qu'aucun objet n'est utilisé à la fois pour la lecture et l'écriture, qu'il existe au maximum un écrivain et, pour les files d'attente synchronisées, il n'y a pas plus d'un en lecture seule.

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

Créer le deuxième objet MessageQueue

Le second côté de la file d'attente de messages est créé à l'aide d'un Objet MQDescriptor obtenu du premier côté. La L'objet MQDescriptor est envoyé via un appel RPC HIDL ou AIDL au processus qui contient la seconde extrémité de la file d'attente des messages. La MQDescriptor contient des informations sur la file d'attente, y compris:

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

L'objet MQDescriptor permet de construire Objet MessageQueue:

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

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

Files d'attente de blocage et indicateurs d'événement

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

  • Format court, avec trois paramètres (pointeur de données, nombre d'éléments, le délai avant expiration). Permet le blocage d'opérations de lecture/écriture individuelles sur un seul file d'attente. Lorsque vous utilisez ce formulaire, la file d'attente gère l'indicateur d'événement et les masques de bits. en interne. Le premier objet de file d'attente de messages doit être initialisé avec un deuxième paramètre de true. Par exemple:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Version longue, avec six paramètres (indicateur d'événement et masques de bits inclus). Prise en charge de 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, Un indicateur d'événement et des masques de bits doivent être fournis à chaque appel de lecture et d'écriture.

Pour le format long, le EventFlag peut être fourni explicitement dans chaque appel readBlocking() et writeBlocking(). L'une des valeurs suivantes : les files d'attente peuvent être initialisées avec un indicateur d'événement interne, qui doit ensuite être extraites des objets MessageQueue de cette file d'attente à l'aide de getEventFlagWord() et utilisé pour créer EventFlag objets dans chaque processus afin de les utiliser avec d'autres FMQ. Le champ Les objets EventFlag peuvent être initialisés avec n'importe quel élément partagé approprié. mémoire.

En général, chaque file d'attente ne doit utiliser qu'un seul type de vidéos longues ou de vidéos longues. Ce n'est pas une erreur, mais faites attention la programmation est nécessaire pour obtenir le résultat souhaité.

Marquer le souvenir en lecture seule

Par défaut, la mémoire partagée dispose d'autorisations de lecture et d'écriture. Pour les appareils désynchronisés files d'attente (kUnsynchronizedWrite), le rédacteur peut supprimer les autorisations d'écriture pour toutes des lecteurs avant de distribuer les objets MQDescriptorUnsync. Cela garantit que l'autre les processus ne peuvent pas écrire dans la file d'attente, ce qui est recommandé pour se protéger contre les bugs ou les comportements insatisfaisants les processus du lecteur. Si le rédacteur souhaite que les lecteurs puissent réinitialiser la file d'attente à chaque fois qu'ils utilisent la commande MQDescriptorUnsync pour créer le côté lecture de la file d'attente, la mémoire ne peut alors pas être marquée en lecture seule. Il s'agit du comportement par défaut du constructeur "MessageQueue". Donc, s’il y a déjà aux utilisateurs existants de cette file d'attente, leur code doit être modifié pour construire la file d'attente avec resetPointer=false

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

Utiliser MessageQueue

L'API publique de l'objet MessageQueue est la suivante:

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

Vous pouvez utiliser availableToWrite() et availableToRead() pour déterminer la quantité de données pouvant être transférées en une seule opération. Dans un file d'attente dé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 peut déborder ; availableToRead() peut donc renvoyer une valeur supérieure à la taille de la file d'attente. La première lecture après un débordement échoue et aboutit la position de lecture de ce lecteur étant définie sur le pointeur d'écriture actuel, si le débordement a été signalé ou non via availableToRead()

Les méthodes read() et write() renvoient true si toutes les données demandées peuvent être (et ont été) transférées vers/depuis dans la file d'attente. Ces méthodes ne bloquent pas : soit elles réussissent (et renvoient true) ou échouer immédiatement (false).

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

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

readBlocking() attend les writeNotification bits ; Si ce paramètre est défini sur 0, l'appel échoue toujours. Si le la valeur de readNotification est 0, l'appel n'échoue pas, mais la lecture réussie ne définit aucun bit de notification. Dans une file d'attente synchronisée, cela signifie que l'appel writeBlocking() correspondant ne se réveille jamais, sauf si le bit est défini ailleurs. Dans une file d'attente non synchronisée, writeBlocking() n'attend pas (il doit tout de même être utilisé pour définir le d'écriture), et il est approprié pour les lectures de ne pas définir bits de notification. De même, writeblocking() échoue si readNotification est défini sur 0, et une écriture réussie définit la valeur writeNotification bits.

Pour attendre sur plusieurs files d'attente à la fois, utilisez un objet EventFlag wait() pour attendre un masque de bits des notifications. La La méthode wait() renvoie un mot d'état avec les bits à l'origine du problème l'heure du réveil. Ces informations sont ensuite utilisées pour vérifier que la file d'attente correspondante a d'espace ou de données pour effectuer l'opération d'écriture/lecture souhaitée, write()/read() non bloquantes. Pour obtenir une opération post , appelez de nouveau la fonction EventFlag wake(). Pour connaître la définition de EventFlag d'abstraction, désignez system/libfmq/include/fmq/EventFlag.h.

Opérations sans copie

La write read/readBlocking/writeBlocking() Les API utilisent un pointeur vers un tampon d'entrée/sortie en tant qu'argument et utilisent des appels memcpy() en interne pour copier des données entre celui-ci et Tampon circulaire FMQ. Pour améliorer les performances, Android 8.0 et les versions ultérieures incluent un ensemble Les API qui fournissent un accès direct du pointeur dans le tampon circulaire, éliminant ainsi les besoin d'utiliser les 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 l'anneau FMQ. tampon. Une fois les données écrites, validez-les à l'aide de commitWrite(). Les méthodes beginRead/commitRead fonctionnent de la même manière.
  • Les méthodes beginRead/Write prennent en entrée la le nombre de messages à lire/écrire et renvoie une valeur booléenne indiquant si le lecture/écriture est possible. Si la lecture ou l'écriture est possible, le memTx struct contient des pointeurs de base pouvant être utilisés pour le pointeur direct l'accès à la mémoire partagée du tampon de l'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 mémoire) et la longueur en en termes de T (longueur du bloc de mémoire par rapport au paramètre HIDL défini type de la file d'attente des messages).
  • La structure MemTransaction contient deux objets MemRegion structs, first et second en tant que lecture ou écriture dans le tampon circulaire peut nécessiter une encapsulation en début de file d'attente. Ce signifierait que deux pointeurs de base sont nécessaires pour lire/écrire des données dans le FMQ de la mémoire tampon en anneau.

Pour obtenir l'adresse de base et la longueur à partir d'une structure MemRegion, procédez comme suit:

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 éléments MemRegion dans une 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 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 l'élément MemRegions qui font partie de ce MemTransaction . Si l'objet MemTransaction représente la mémoire régions pour lire/écrire N éléments de type T, puis la plage valide de idx est compris entre 0 et N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Écrire nMessages éléments de type T dans les régions de mémoire décrites par l'objet, à partir de l'index startIdx. Cette méthode utilise memcpy() et n'est pas destiné à être utilisé pour une copie sans zéro opération. Si l'objet MemTransaction représente la mémoire à lire/écrire N éléments de type T, la plage valide pour idx est compris entre 0 et N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Méthode d'assistance pour lire nMessages éléments de type T à partir de régions de mémoire décrites par l'objet à partir de startIdx. Ce utilise memcpy() et n'est pas destinée à être utilisée pour une copie sans opération.

Envoyer 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 sur plusieurs files d'attente en transmettant une EventFlag en la forme longue de readBlocking()/writeBlocking(), vous pouvez extraire Pointeur d'indicateur d'événement (avec getEventFlagWord()) à partir d'un MessageQueue qui a été initialisé pour créer l'indicateur utilisez cette option pour créer l'objet EventFlag nécessaire.
  4. Utilisez la méthode getDesc() MessageQueue pour obtenir une Descriptor.
  5. Dans le fichier .hal, attribuez à la méthode un paramètre de type. fmq_sync ou fmq_unsync, où T est un type défini par HIDL approprié. Utilisez ce champ pour envoyer l'objet renvoyé par getDesc() au processus de réception.

Côté réception:

  1. Utilisez l'objet descripteur pour créer un objet MessageQueue. Être assurez-vous d'utiliser le même type de file d'attente et le même type de données, sinon le modèle ne parvient pas compiler.
  2. Si vous avez extrait un indicateur d'événement, vous devez l'extraire de la table MessageQueue dans le processus de réception.
  3. Utilisez l'objet MessageQueue pour transférer les données.