Fast Message Queue (FMQ)

Wenn Sie AIDL-Unterstützung suchen, sehen Sie sich auch FMQ mit AIDL an.

Die RPC-Infrastruktur (Remote Procedure Call) von HIDL verwendet Binder-Mechanismen, was bedeutet, dass Aufrufe Overhead verursachen, Kernel-Operationen erfordern und möglicherweise Scheduler-Aktionen auslösen. In Fällen, in denen Daten zwischen Prozessen mit geringerem Overhead und ohne Kernel-Beteiligung übertragen werden müssen, wird jedoch das Fast Message Queue (FMQ)-System verwendet.

FMQ erstellt Nachrichtenwarteschlangen mit den gewünschten Eigenschaften. Ein MQDescriptorSync oder MQDescriptorUnsync Objekt kann über einen HIDL-RPC-Aufruf gesendet und vom empfangenden Prozess für den Zugriff auf die Nachrichtenwarteschlange verwendet werden.

Fast Message Queues werden nur in C++ und auf Geräten mit Android 8.0 und höher unterstützt.

MessageQueue-Typen

Android unterstützt zwei Warteschlangentypen (bekannt als Flavors ):

  • Nicht synchronisierte Warteschlangen dürfen überlaufen und können viele Leser haben; Jeder Leser muss die Daten rechtzeitig lesen, sonst gehen sie verloren.
  • Synchronisierte Warteschlangen dürfen nicht überlaufen und können nur einen Leser haben.

Beide Warteschlangentypen dürfen keinen Unterlauf aufweisen (das Lesen aus einer leeren Warteschlange schlägt fehl) und können nur einen Schreiber haben.

Unsynchronisiert

Eine nicht synchronisierte Warteschlange hat nur einen Schreiber, kann aber eine beliebige Anzahl von Lesern haben. Es gibt eine Schreibposition für die Warteschlange; jedoch behält jeder Leser seine eigene unabhängige Leseposition im Auge.

Schreibvorgänge in die Warteschlange sind immer erfolgreich (werden nicht auf Überlauf überprüft), solange sie nicht größer als die konfigurierte Warteschlangenkapazität sind (Schreibvorgänge, die größer als die Warteschlangenkapazität sind, schlagen sofort fehl). Da jeder Leser möglicherweise eine andere Leseposition hat, können Daten aus der Warteschlange fallen, wenn neue Schreibvorgänge den Platz benötigen, anstatt darauf zu warten, dass jeder Leser alle Daten liest.

Die Leser sind dafür verantwortlich, Daten abzurufen, bevor sie am Ende der Warteschlange landen. Ein Lesevorgang, der versucht, mehr Daten zu lesen, als verfügbar sind, schlägt entweder sofort fehl (falls nicht blockierend) oder wartet, bis genügend Daten verfügbar sind (falls blockierend). Ein Lesevorgang, der versucht, mehr Daten als die Warteschlangenkapazität zu lesen, schlägt immer sofort fehl.

Wenn ein Lesegerät nicht mit dem Schreibgerät mithalten kann und die von diesem Lesegerät geschriebene und noch nicht gelesene Datenmenge größer ist als die Warteschlangenkapazität, werden beim nächsten Lesevorgang keine Daten zurückgegeben. Stattdessen wird die Leseposition des Lesegeräts auf die letzte Schreibposition zurückgesetzt und dann ein Fehler zurückgegeben. Wenn die zum Lesen verfügbaren Daten nach einem Überlauf, aber vor dem nächsten Lesevorgang überprüft werden, werden mehr zum Lesen verfügbare Daten als die Warteschlangenkapazität angezeigt, was darauf hinweist, dass ein Überlauf aufgetreten ist. (Wenn die Warteschlange zwischen der Überprüfung der verfügbaren Daten und dem Versuch, diese Daten zu lesen, überläuft, ist der einzige Hinweis auf einen Überlauf, dass der Lesevorgang fehlschlägt.)

Leser einer nicht synchronisierten Warteschlange möchten wahrscheinlich die Lese- und Schreibzeiger der Warteschlange nicht zurücksetzen. Daher sollten Leser beim Erstellen der Warteschlange aus dem Deskriptor ein „false“-Argument für den „resetPointers“-Parameter verwenden.

Synchronisiert

Eine synchronisierte Warteschlange verfügt über einen Schreiber und einen Leser mit einer einzelnen Schreibposition und einer einzelnen Leseposition. Es ist nicht möglich, mehr Daten zu schreiben, als in der Warteschlange Platz ist, oder mehr Daten zu lesen, als die Warteschlange derzeit enthält. Abhängig davon, ob die blockierende oder nicht blockierende Schreib- oder Lesefunktion aufgerufen wird, schlagen Versuche, den verfügbaren Speicherplatz oder die verfügbaren Daten zu überschreiten, entweder sofort fehl oder werden blockiert, bis der gewünschte Vorgang abgeschlossen werden kann. Versuche, mehr Daten zu lesen oder zu schreiben, als die Warteschlangenkapazität übersteigt, schlagen immer sofort fehl.

Einrichten eines FMQ

Für eine Nachrichtenwarteschlange sind mehrere MessageQueue Objekte erforderlich: eines zum Schreiben und eines oder mehrere zum Lesen. Es gibt keine explizite Konfiguration, welches Objekt zum Schreiben oder Lesen verwendet wird; Es obliegt dem Benutzer, sicherzustellen, dass kein Objekt sowohl zum Lesen als auch zum Schreiben verwendet wird, dass es höchstens einen Schreiber und bei synchronisierten Warteschlangen höchstens einen Leser gibt.

Erstellen des ersten MessageQueue-Objekts

Eine Nachrichtenwarteschlange wird mit einem einzigen Aufruf erstellt und konfiguriert:

#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 */);
  • Der MessageQueue<T, flavor>(numElements) -Initialisierer erstellt und initialisiert ein Objekt, das die Funktionalität der Nachrichtenwarteschlange unterstützt.
  • Der MessageQueue<T, flavor>(numElements, configureEventFlagWord) -Initialisierer erstellt und initialisiert ein Objekt, das die Nachrichtenwarteschlangenfunktionalität mit Blockierung unterstützt.
  • flavor kann entweder kSynchronizedReadWrite für eine synchronisierte Warteschlange oder kUnsynchronizedWrite für eine nicht synchronisierte Warteschlange sein.
  • uint16_t (in diesem Beispiel) kann ein beliebiger HIDL-definierter Typ sein, der keine verschachtelten Puffer (keine string oder vec -Typen), Handles oder Schnittstellen umfasst.
  • kNumElementsInQueue gibt die Größe der Warteschlange in Anzahl der Einträge an; Es bestimmt die Größe des gemeinsam genutzten Speicherpuffers, der der Warteschlange zugewiesen wird.

Erstellen des zweiten MessageQueue-Objekts

Die zweite Seite der Nachrichtenwarteschlange wird mithilfe eines MQDescriptor Objekts erstellt, das von der ersten Seite erhalten wurde. Das MQDescriptor Objekt wird über einen HIDL- oder AIDL-RPC-Aufruf an den Prozess gesendet, der das zweite Ende der Nachrichtenwarteschlange enthält. Der MQDescriptor enthält Informationen über die Warteschlange, einschließlich:

  • Informationen zum Zuordnen des Puffers und des Schreibzeigers.
  • Informationen zum Zuordnen des Lesezeigers (wenn die Warteschlange synchronisiert ist).
  • Informationen zum Zuordnen des Ereignisflagworts (wenn die Warteschlange blockiert).
  • Objekttyp ( <T, flavor> ), der den HIDL-definierten Typ von Warteschlangenelementen und den Warteschlangentyp (synchronisiert oder nicht synchronisiert) umfasst.

Das MQDescriptor Objekt kann zum Erstellen eines MessageQueue Objekts verwendet werden:

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

Der Parameter resetPointers gibt an, ob die Lese- und Schreibpositionen beim Erstellen dieses MessageQueue Objekts auf 0 zurückgesetzt werden sollen. In einer nicht synchronisierten Warteschlange wird die Leseposition (die für jedes MessageQueue Objekt in nicht synchronisierten Warteschlangen lokal ist) während der Erstellung immer auf 0 gesetzt. Normalerweise wird der MQDescriptor während der Erstellung des ersten Nachrichtenwarteschlangenobjekts initialisiert. Für zusätzliche Kontrolle über den gemeinsam genutzten Speicher können Sie den MQDescriptor manuell einrichten ( MQDescriptor ist in system/libhidl/base/include/hidl/MQDescriptor.h definiert) und dann jedes MessageQueue Objekt wie in diesem Abschnitt beschrieben erstellen.

Blockierende Warteschlangen und Ereignisflags

Standardmäßig unterstützen Warteschlangen das Blockieren von Lese-/Schreibvorgängen nicht. Es gibt zwei Arten blockierender Lese-/Schreibaufrufe:

  • Kurzform mit drei Parametern (Datenzeiger, Anzahl der Elemente, Timeout). Unterstützt das Blockieren einzelner Lese-/Schreibvorgänge in einer einzelnen Warteschlange. Bei Verwendung dieser Form verarbeitet die Warteschlange das Ereignisflag und die Bitmasken intern und das erste Nachrichtenwarteschlangenobjekt muss mit dem zweiten Parameter true initialisiert werden. Beispiel:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Langform mit sechs Parametern (einschließlich Ereignisflag und Bitmasken). Unterstützt die Verwendung eines gemeinsam genutzten EventFlag Objekts zwischen mehreren Warteschlangen und ermöglicht die Angabe der zu verwendenden Benachrichtigungsbitmasken. In diesem Fall müssen das Ereignisflag und die Bitmasken bei jedem Lese- und Schreibaufruf bereitgestellt werden.

Für die Langform kann das EventFlag explizit in jedem readBlocking() und writeBlocking() Aufruf angegeben werden. Eine der Warteschlangen kann mit einem internen Ereignisflag initialisiert werden, das dann mit getEventFlagWord() aus den MessageQueue Objekten dieser Warteschlange extrahiert und zum Erstellen EventFlag Objekten in jedem Prozess zur Verwendung mit anderen FMQs verwendet werden muss. Alternativ können die EventFlag Objekte mit jedem geeigneten Shared Memory initialisiert werden.

Im Allgemeinen sollte jede Warteschlange nur eine der Optionen Nicht-Blockierung, Kurzform-Blockierung oder Langform-Blockierung verwenden. Es ist kein Fehler, sie zu mischen, aber um das gewünschte Ergebnis zu erzielen, ist eine sorgfältige Programmierung erforderlich.

Markieren Sie den Speicher als schreibgeschützt

Standardmäßig verfügt der gemeinsam genutzte Speicher über Lese- und Schreibberechtigungen. Bei nicht synchronisierten Warteschlangen ( kUnsynchronizedWrite ) möchte der Autor möglicherweise die Schreibberechtigungen für alle Leser entfernen, bevor er die MQDescriptorUnsync Objekte ausgibt. Dadurch wird sichergestellt, dass die anderen Prozesse nicht in die Warteschlange schreiben können. Dies wird zum Schutz vor Fehlern oder schlechtem Verhalten in den Leseprozessen empfohlen. Wenn der Autor möchte, dass die Leser die Warteschlange jedes Mal zurücksetzen können, wenn sie MQDescriptorUnsync zum Erstellen der Leseseite der Warteschlange verwenden, kann der Speicher nicht als schreibgeschützt markiert werden. Dies ist das Standardverhalten des „MessageQueue“-Konstruktors. Wenn es also bereits Benutzer dieser Warteschlange gibt, muss deren Code geändert werden, um die Warteschlange mit resetPointer=false zu erstellen.

  • Autor: Rufen Sie ashmem_set_prot_region mit einem MQDescriptor Dateideskriptor und einer schreibgeschützten Region ( PROT_READ ) auf:
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Leser: Nachrichtenwarteschlange mit resetPointer=false erstellen (der Standardwert ist true ):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Verwenden der MessageQueue

Die öffentliche API des MessageQueue Objekts ist:

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() und availableToRead() kann ermittelt werden, wie viele Daten in einem einzigen Vorgang übertragen werden können. In einer nicht synchronisierten Warteschlange:

  • availableToWrite() gibt immer die Kapazität der Warteschlange zurück.
  • Jeder Leser hat seine eigene Leseposition und führt seine eigene Berechnung für availableToRead() durch.
  • Aus der Sicht eines langsamen Lesers darf die Warteschlange überlaufen; Dies kann dazu führen, dass availableToRead() einen Wert zurückgibt, der größer als die Größe der Warteschlange ist. Der erste Lesevorgang nach einem Überlauf schlägt fehl und führt dazu, dass die Leseposition für diesen Leser auf den aktuellen Schreibzeiger gesetzt wird, unabhängig davon, ob der Überlauf über availableToRead() gemeldet wurde oder nicht.

Die Methoden read() und write() geben true zurück, wenn alle angeforderten Daten in die/aus der Warteschlange übertragen werden konnten (und wurden). Diese Methoden blockieren nicht; Sie sind entweder erfolgreich (und geben true zurück) oder geben sofort einen Fehler ( false ) zurück.

Die Methoden readBlocking() und writeBlocking() warten, bis der angeforderte Vorgang abgeschlossen werden kann oder bis eine Zeitüberschreitung auftritt (ein timeOutNanos Wert von 0 bedeutet, dass es nie zu einer Zeitüberschreitung kommt).

Sperrvorgänge werden über ein Ereignismerkerwort realisiert. Standardmäßig erstellt und verwendet jede Warteschlange ihr eigenes Flag-Wort, um die Kurzform von readBlocking() und writeBlocking() zu unterstützen. Es ist möglich, dass mehrere Warteschlangen ein einzelnes Wort gemeinsam nutzen, sodass ein Prozess auf Schreib- oder Lesevorgänge in einer der Warteschlangen warten kann. Ein Zeiger auf das Ereignisflagwort einer Warteschlange kann durch Aufrufen getEventFlagWord() abgerufen werden. Dieser Zeiger (oder ein beliebiger Zeiger auf einen geeigneten gemeinsam genutzten Speicherort) kann verwendet werden, um ein EventFlag Objekt zu erstellen, das in die Langform von readBlocking() und übergeben wird writeBlocking() für eine andere Warteschlange. Die Parameter readNotification und writeNotification geben an, welche Bits im Ereignisflag verwendet werden sollen, um Lese- und Schreibvorgänge in dieser Warteschlange zu signalisieren. readNotification und writeNotification sind 32-Bit-Bitmasken.

readBlocking() wartet auf die writeNotification Bits; Wenn dieser Parameter 0 ist, schlägt der Aufruf immer fehl. Wenn der readNotification Wert 0 ist, schlägt der Aufruf nicht fehl, aber ein erfolgreicher Lesevorgang setzt keine Benachrichtigungsbits. In einer synchronisierten Warteschlange würde dies bedeuten, dass der entsprechende writeBlocking() Aufruf niemals aufwacht, es sei denn, das Bit ist an anderer Stelle gesetzt. In einer nicht synchronisierten Warteschlange wartet writeBlocking() nicht (es sollte weiterhin zum Setzen des Schreibbenachrichtigungsbits verwendet werden) und es ist angemessen, dass bei Lesevorgängen keine Benachrichtigungsbits gesetzt werden. Ebenso schlägt writeblocking() fehl, wenn readNotification 0 ist und ein erfolgreicher Schreibvorgang die angegebenen writeNotification Bits setzt.

Um in mehreren Warteschlangen gleichzeitig zu warten, verwenden Sie die wait() Methode eines EventFlag Objekts, um auf eine Bitmaske von Benachrichtigungen zu warten. Die Methode wait() gibt ein Statuswort mit den Bits zurück, die das Aufwecken verursacht haben. Diese Informationen werden dann verwendet, um zu überprüfen, ob in der entsprechenden Warteschlange genügend Speicherplatz oder Daten für den gewünschten Schreib-/Lesevorgang vorhanden sind, und um einen nicht blockierenden write() / read() auszuführen. Um eine Benachrichtigung nach dem Vorgang zu erhalten, verwenden Sie einen weiteren Aufruf der wake() Methode von EventFlag . Eine Definition der EventFlag Abstraktion finden Sie unter system/libfmq/include/fmq/EventFlag.h .

Keine Kopiervorgänge

Die read / write / readBlocking / writeBlocking() APIs nehmen einen Zeiger auf einen Eingabe-/Ausgabepuffer als Argument und verwenden memcpy() Aufrufe intern, um Daten zwischen demselben und dem FMQ-Ringpuffer zu kopieren. Um die Leistung zu verbessern, enthalten Android 8.0 und höher eine Reihe von APIs, die einen direkten Zeigerzugriff auf den Ringpuffer ermöglichen, sodass keine memcpy Aufrufe erforderlich sind.

Verwenden Sie die folgenden öffentlichen APIs für Zero-Copy-FMQ-Vorgänge:

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);
  • Die beginWrite Methode stellt Basiszeiger in den FMQ-Ringpuffer bereit. Nachdem die Daten geschrieben wurden, schreiben Sie sie mit commitWrite() fest. Die Methoden beginRead / commitRead verhalten sich auf die gleiche Weise.
  • Die Methoden beginRead / Write nehmen als Eingabe die Anzahl der zu lesenden/schreibenden Nachrichten und geben einen booleschen Wert zurück, der angibt, ob das Lesen/Schreiben möglich ist. Wenn das Lesen oder Schreiben möglich ist, wird die memTx Struktur mit Basiszeigern gefüllt, die für den direkten Zeigerzugriff auf den gemeinsam genutzten Ringpufferspeicher verwendet werden können.
  • Die MemRegion Struktur enthält Details zu einem Speicherblock, einschließlich des Basiszeigers (Basisadresse des Speicherblocks) und der Länge in Bezug auf T (Länge des Speicherblocks in Bezug auf den HIDL-definierten Typ der Nachrichtenwarteschlange).
  • Die MemTransaction Struktur enthält zwei MemRegion Strukturen, first und second , da ein Lese- oder Schreibvorgang in den Ringpuffer möglicherweise einen Umlauf bis zum Anfang der Warteschlange erfordert. Dies würde bedeuten, dass zum Lesen/Schreiben von Daten in den FMQ-Ringpuffer zwei Basiszeiger erforderlich sind.

So erhalten Sie die Basisadresse und -länge aus einer MemRegion Struktur:

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

So erhalten Sie Verweise auf die erste und zweite MemRegion s innerhalb eines MemTransaction Objekts:

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

Beispiel für einen Schreibvorgang in den FMQ mit Zero-Copy-APIs:

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
}

Die folgenden Hilfsmethoden sind ebenfalls Teil von MemTransaction :

  • T* getSlot(size_t idx);
    Gibt einen Zeiger auf die Slot- idx innerhalb der MemRegions zurück, die Teil dieses MemTransaction Objekts sind. Wenn das MemTransaction Objekt die Speicherbereiche zum Lesen/Schreiben von N Elementen vom Typ T darstellt, liegt der gültige Bereich von idx zwischen 0 und N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Schreiben Sie nMessages Elemente vom Typ T in die vom Objekt beschriebenen Speicherbereiche, beginnend mit dem Index startIdx . Diese Methode verwendet memcpy() und ist nicht für einen Zero-Copy-Vorgang gedacht. Wenn das MemTransaction Objekt Speicher zum Lesen/Schreiben von N Elementen vom Typ T darstellt, liegt der gültige Bereich von idx zwischen 0 und N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Hilfsmethode zum Lesen nMessages Elementen vom Typ T aus den Speicherbereichen, die vom Objekt beschrieben werden, beginnend mit startIdx . Diese Methode verwendet memcpy() und ist nicht für einen Zero-Copy-Vorgang gedacht.

Senden der Warteschlange über HIDL

Auf der erstellenden Seite:

  1. Erstellen Sie ein Nachrichtenwarteschlangenobjekt wie oben beschrieben.
  2. Überprüfen Sie mit isValid() ob das Objekt gültig ist.
  3. Wenn Sie in mehreren Warteschlangen warten, indem Sie ein EventFlag in der Langform von readBlocking() / writeBlocking() übergeben, können Sie den Ereignis-Flag-Zeiger (mit getEventFlagWord() ) aus einem MessageQueue Objekt extrahieren, das zum Erstellen des Flags initialisiert wurde. und verwenden Sie dieses Flag, um das erforderliche EventFlag Objekt zu erstellen.
  4. Verwenden Sie die MessageQueue Methode getDesc() um ein Deskriptorobjekt abzurufen.
  5. Geben Sie der Methode in der .hal Datei einen Parameter vom Typ fmq_sync oder fmq_unsync wobei T ein geeigneter HIDL-definierter Typ ist. Verwenden Sie dies, um das von getDesc() zurückgegebene Objekt an den empfangenden Prozess zu senden.

Auf der Empfangsseite:

  1. Verwenden Sie das Deskriptorobjekt, um ein MessageQueue Objekt zu erstellen. Stellen Sie sicher, dass Sie dieselbe Warteschlangenvariante und denselben Datentyp verwenden, da die Vorlage sonst nicht kompiliert werden kann.
  2. Wenn Sie ein Ereignisflag extrahiert haben, extrahieren Sie das Flag aus dem entsprechenden MessageQueue Objekt im Empfangsprozess.
  3. Verwenden Sie das MessageQueue Objekt, um Daten zu übertragen.