Kolejka wiadomości (FMQ)

Jeśli chcesz skorzystać z obsługi AIDL, zapoznaj się też FMQ z AIDL.

Infrastruktura zdalnego wywoływania procedur HIDL wykorzystuje mechanizmy Binder, co oznacza, że wywołania wiążą się z nadmiarem pracy, wymagają działania jądra i mogą aktywować działanie algorytmu szeregowania. Jednak w przypadkach, w których dane muszą być przenoszone między z mniejszym nakładem pracy i bez wykorzystania jądra systemu używany jest system FMQ.

FMQ tworzy kolejki wiadomości z odpowiednimi właściwościami. An Obiekt MQDescriptorSync lub MQDescriptorUnsync może być wysyłane przez wywołanie HIDL i używane przez proces odbierający do uzyskiwania dostępu do w kolejce wiadomości.

Kolejki szybkich wiadomości są obsługiwane tylko w języku C++ i na urządzeniach z Androidem 8.0 lub nowszym.

Typy MessageQueue

Android obsługuje 2 rodzaje kolejek (rodzaje):

  • Niezsynchronizowane kolejki mogą przekraczać limit i mogą zawierać czytelnicy; każdy czytelnik musi odczytać dane w odpowiednim czasie – w przeciwnym razie je utracisz.
  • Zsynchronizowane kolejki nie mogą się przepełniać i mogą mieć jednego czytelnika.

Oba typy kolejek nie mogą być niedopełnione (odczyt z pustej kolejki) nie powiodła się) i może mieć tylko jednego zapisującego.

Nie zsynchronizowano

Niezsynchronizowana kolejka ma tylko jednego zapisującego, ale może mieć dowolną liczbę czytelników. Kolejka ma 1 pozycję zapisu. jednak każdy czytelnik zachowuje własnej, niezależnej pozycji odczytu.

Zapisy w kolejce zawsze się udają (nie są sprawdzane pod kątem przepełnienia), dopóki nie są większe niż skonfigurowana pojemność kolejki (zapisy są większe niż możliwości natychmiastowego działania kolejki nie udają się). Każdy czytnik może czytać coś innego zamiast czekać, aż każdy czytelnik zapozna się z każdym fragmentem danych, mogą zniknąć z kolejki za każdym razem, gdy nowe zapisy potrzebują wolnego miejsca.

Czytelnicy są odpowiedzialni za pobranie danych, zanim znikną z końca w kolejce. Odczyt, który próbuje odczytać więcej danych, niż jest dostępnych kończy się niepowodzeniem od razu (jeśli nie jest blokowane) lub czeka na zgromadzenie wystarczającej ilości danych (jeśli blokowanie). Odczyt, który próbuje odczytać więcej danych niż wynosi zawsze pojemność kolejki nie działa od razu.

Jeśli czytelnik nie dotrzymuje kroku autorowi, przez co ilość danych zapisane, ale jeszcze nieprzeczytane przez tego czytnika, przekracza pojemność kolejki, następny odczyt nie zwraca danych; odczytywane przez , aby była równa ostatniej pozycji zapisu, a następnie zwraca błąd. Jeśli dane dostępne do odczytu są sprawdzane po ich przekroczeniu, ale przed kolejnym odczytem pokazuje więcej danych dostępnych do odczytu niż pojemność kolejki, co oznacza wystąpiło przepełnienie. (Jeśli kolejka wystarcza na sprawdzenie dostępnych danych i próbując odczytać te dane, jedynym oznakem przepełnienia jest to, że odczyt nie powiedzie się).

Czytelnicy niezsynchronizowanej kolejki prawdopodobnie nie chcą resetować się wskaźniki do odczytu i zapisu w kolejce. Podczas tworzenia kolejki na podstawie czytelnicy deskryptorów powinni używać argumentu „false” dla argumentu „resetPointers” .

Synchronizowany

Zsynchronizowana kolejka ma 1 zapisującego i 1 czytelnika z 1 zapisem i jedną pozycję odczytu. Nie da się zapisać więcej danych niż kolejka zawiera miejsce na dane lub do odczytu więcej danych niż bieżąca kolejka. W zależności od tego, czy jest próbuje przekroczyć dostępną przestrzeń lub dane albo nie uda się zwrócić natychmiast lub blokować do czasu zakończenia wybranej operacji. Próby odczyt lub zapis danych przekraczających pojemność kolejki zawsze kończy się natychmiastowo.

Skonfiguruj FMQ

Kolejka wiadomości wymaga wielu obiektów MessageQueue: 1 do gdzie można zapisać treść i co najmniej jeden być odczytywany. Nie ma wyraźnych konfiguracja obiektu używanego do zapisu lub odczytu; zależy od aby upewnić się, że żaden obiekt nie jest używany do odczytu i zapisu, jest maksymalnie jeden zapisujący, a dla zsynchronizowanych kolejek – .

Tworzenie pierwszego obiektu MessageQueue

Kolejka wiadomości jest tworzona i konfigurowana z pojedynczym wywołaniem:

#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 */);
  • Inicjator MessageQueue<T, flavor>(numElements) tworzy i inicjuje obiekt, który obsługuje funkcję kolejki wiadomości.
  • Inicjator MessageQueue<T, flavor>(numElements, configureEventFlagWord) tworzy i inicjuje obiekt obsługujące funkcję kolejki wiadomości z blokowaniem.
  • flavor może mieć wartość kSynchronizedReadWrite w przypadku synchronizowana kolejka lub kUnsynchronizedWrite w przypadku niezsynchronizowanej kolejki kolejkę.
  • uint16_t (w tym przykładzie) może być dowolnym Typ określony przez HIDL, który nie zawiera zagnieżdżonych buforów (bez string ani vec) różne typy komponentów), uchwyty i interfejsy.
  • kNumElementsInQueue wskazuje rozmiar kolejki w liczbie wpisy; określa rozmiar przydzielonego bufora pamięci współdzielonej dla kolejki.

Utwórz drugi obiekt MessageQueue.

Druga strona kolejki wiadomości jest tworzona za pomocą Obiekt MQDescriptor został pobrany z pierwszej strony. Obiekt MQDescriptor jest wysyłany do procesu przez wywołanie HIDL lub AIDL RPC który zawiera drugi koniec kolejki wiadomości. MQDescriptor zawiera informacje o kolejce, w tym:

  • Informacje do mapowania bufora i wskaźnika zapisu.
  • informacje do mapowania wskaźnika odczytu (jeśli kolejka jest zsynchronizowana).
  • Informacje do mapowania słowa flagi zdarzenia (jeśli kolejka jest blokowana).
  • Typ obiektu (<T, flavor>), który obejmuje Zdefiniowany przez HDL typ: elementów i rodzaju kolejki (zsynchronizowanych lub niezsynchronizowanych),

Obiektu MQDescriptor można użyć do utworzenia Obiekt MessageQueue:

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

Parametr resetPointers wskazuje, czy zresetować odczyt i zapisz pozycje jako 0 podczas tworzenia tego obiektu MessageQueue. W niezsynchronizowanej kolejce pozycja odczytu (lokalna dla każdej MessageQueue obiekt w niezsynchronizowanych kolejkach) ma zawsze wartość 0 podczas tworzenia. Zwykle MQDescriptor jest inicjowany podczas podczas tworzenia pierwszego obiektu kolejki wiadomości. Dodatkowa kontrola nad udostępnianymi możesz skonfigurować MQDescriptor ręcznie (MQDescriptor jest zdefiniowany w system/libhidl/base/include/hidl/MQDescriptor.h). a następnie utwórz każdy obiekt MessageQueue zgodnie z opisem w tej sekcji.

Blokuj kolejki i flagi zdarzeń

Domyślnie kolejki nie obsługują blokowania odczytu ani zapisu. Są 2 rodzaje blokowania wywołań odczytu/zapisu:

  • Wersja krótka z 3 parametrami (wskaźnikiem danych, liczbą elementów, ). Obsługuje blokowanie pojedynczych operacji odczytu/zapisu na pojedynczym kolejkę. Gdy używasz tego formularza, kolejka obsługuje flagę zdarzenia i maski bitowe wewnętrznie, a pierwszy obiekt kolejki wiadomości musi zostanie zainicjowana drugim parametrem o wartości true. Na przykład:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Wersja długa z 6 parametrami (obejmuje flagę zdarzenia i maski bitowe). Obsługuje używanie wspólnego obiektu EventFlag między wieloma kolejkami i umożliwia określenie masek bitów powiadomień, które mają być używane. W tym przypadku parametr Każde wywołanie odczytu i zapisu musi mieć flagę zdarzenia i maski bitowe.

W przypadku długiej formy właściwość EventFlag należy podać jawnie w za każde połączenie readBlocking() i writeBlocking(). Jedna z tych wartości: kolejki mogą zostać zainicjowane przy użyciu wewnętrznej flagi zdarzenia, którą należy następnie wyodrębnione z obiektów MessageQueue tej kolejki za pomocą funkcji getEventFlagWord() i użyto do utworzenia EventFlag obiektów w każdym procesie do użycia z innymi FMQ. Ewentualnie EventFlag obiektu można zainicjować za pomocą dowolnego odpowiedniego udostępnionego pamięci.

Ogólnie w każdej kolejce powinna być używana tylko jedna z krótkich, nieblokujących treści albo blokowanie długich filmów. Mieszanie tych elementów to nie błąd, ale zachowaj ostrożność do osiągnięcia zamierzonych rezultatów.

Oznacz pamięć jako tylko do odczytu

Domyślnie pamięć współdzielona ma uprawnienia do odczytu i zapisu. Niezsynchronizowane (kUnsynchronizedWrite), autor może usunąć uprawnienia do zapisu wszystkich czytelników, zanim przekaże obiekty MQDescriptorUnsync. Dzięki temu druga strona procesy nie mogą zapisywać w kolejce, co jest zalecane w celu ochrony przed błędami i niewłaściwym działaniem jak interpretuje czytelnika. Jeśli autor chce, aby czytelnicy mogli zresetować kolejkę za każdym razem, gdy używają polecenia MQDescriptorUnsync, aby utworzyć stronę do odczytu kolejki, to pamięci nie można oznaczyć jako tylko do odczytu. Jest to domyślne działanie konstruktora „MessageQueue”. Jeśli więc użytkownik zawiera już z istniejących użytkowników tej kolejki, ich kod należy zmienić, aby utworzyć kolejkę resetPointer=false

  • Zapisujący: wywołaj funkcję ashmem_set_prot_region z deskryptorem pliku MQDescriptor i region ustawiony jako tylko do odczytu (PROT_READ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • Czytnik: utwórz kolejkę wiadomości za pomocą funkcji resetPointer=false wartość domyślna to true):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

Używanie MessageQueue

Publiczny interfejs API obiektu MessageQueue:

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

Można użyć availableToWrite() i availableToRead() aby określić, ile danych można przenieść w ramach pojedynczej operacji. W niezsynchronizowana kolejka:

  • Funkcja availableToWrite() zawsze zwraca pojemność kolejki.
  • Każdy czytelnik ma własne miejsce odczytu i wykonuje własne obliczenia availableToRead()
  • Z punktu widzenia powolnego czytelnika kolejka może się przepełnić. może to spowodować, że funkcja availableToRead() zwróci wartość większą niż od rozmiaru kolejki. Pierwszy odczyt po przekroczeniu limitu kończy się niepowodzeniem i powoduje pozycja odczytu tego czytnika ustawiona na bieżący wskaźnik zapisu, czy zgłoszenie przepełnienia zostało zgłoszone availableToRead()

Metody read() i write() zwracają true, jeśli można przenieść (i być) wszystkie żądane dane do/z w kolejce. Te metody nie blokują dostępu. mu się to uda (i zwróci true) lub niepowodzenie zwrotu (false) natychmiast.

Metody readBlocking() i writeBlocking() czekają aż do zakończenia żądanej operacji lub do momentu, gdy upłynie czas oczekiwania (a Wartość timeOutNanos o wartości 0 oznacza, że nigdy nie przekroczono limitu czasu.

Operacje blokowania są implementowane za pomocą słowa flagi zdarzenia. Domyślnie każda kolejka tworzy i używa własnych flag do obsługi krótkich readBlocking() i writeBlocking(). To możliwe, wiele kolejek do współdzielenia 1 słowa, tak aby proces mógł czekać na zapisy lub do dowolnej z kolejek. Wskaźnikiem do słowa flagi zdarzenia kolejki może być uzyskane przez wywołanie metody getEventFlagWord() i ten wskaźnik (lub dowolny wskaźnik do odpowiedniej lokalizacji współdzielonej pamięci) można wykorzystać do utworzenia EventFlag obiekt, który ma zostać przesłany do długiej postaci readBlocking() i writeBlocking() dla innego kolejkę. readNotification i writeNotification które określają, które bity flagi zdarzenia mają być używane do sygnalizowania odczytów i pisze w tej kolejce. readNotification i writeNotification to 32-bitowa maska bitowa.

readBlocking() czeka na writeNotification bitach; jeśli ten parametr ma wartość 0, wywołanie zawsze kończy się niepowodzeniem. Jeśli readNotification ma wartość 0, wywołanie nie kończy się niepowodzeniem, ale udany odczyt nie ustawi żadnych powiadomień. W zsynchronizowanej kolejce oznaczałoby to, że odpowiednie wywołanie writeBlocking() nigdy się nie wybudza, chyba że bit zostanie ustawiony w innym miejscu. W niezsynchronizowanej kolejce Funkcja writeBlocking() nie czeka (nadal należy jej używać do ustawiania zapisu powiadomienia), a odczyty nie mogą ustawiać żadnych powiadomienia. Analogicznie żądanie writeblocking() kończy się niepowodzeniem, jeśli readNotification to 0, a udany zapis ustawia określoną wartość writeNotification bitów.

Aby zaczekać na kilku kolejkach jednocześnie, użyj metody EventFlag Metoda wait() oczekiwania na maskę bitową powiadomień. Metoda wait() zwraca słowo stanu z bitami, które spowodowały Pobudka ustawiona. Te informacje są następnie używane do sprawdzenia, czy odpowiednia kolejka wystarczającą ilość miejsca lub danych do wykonania żądanej operacji zapisu/odczytu, a następnie wykonaj nieblokujące: write()/read(). Aby pobrać operację wysyłania powiadomienie, użyj innego połączenia z numerem EventFlag wake(). Definicja: EventFlag abstrakcja, odnosi się system/libfmq/include/fmq/EventFlag.h.

0 operacji kopiowania

read.write.readBlocking.writeBlocking() Interfejsy API używają wskaźnika do bufora wejściowego/wyjściowego jako argumentu i używają memcpy() wywołuje wewnętrznie, aby skopiować dane między tym samym a Bufor dzwonka FMQ. Aby zwiększyć wydajność, Android 8.0 i nowsze wersje zawierają zestaw interfejsy API zapewniające bezpośredni dostęp wskaźnika do bufora pierścieniowego, eliminujące muszą użyć memcpy połączeń.

Użyj następujących publicznych interfejsów API do operacji FMQ bez kopiowania:

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);
  • Metoda beginWrite dostarcza wskaźniki bazowe do pierścienia FMQ bufora. Po zapisaniu danych zatwierdź je za pomocą commitWrite(). Metody beginRead/commitRead działają tak samo.
  • Metody beginRead/Write przyjmują jako dane wejściowe liczba wiadomości do odczytu/zapisania i zwraca wartość logiczną wskazującą, czy odczyt/zapis jest możliwy. Jeśli odczyt lub zapis jest możliwy, memTx element struct zawiera wskaźniki bazowe, których można używać we wskaźniku bezpośrednim do współdzielonej pamięci bufora dzwonka.
  • Struktura MemRegion zawiera szczegóły bloku pamięci, łącznie ze wskaźnikiem bazowym (adresem podstawowym bloku pamięci) i długością w argumencie wyrażenia T (długość bloku pamięci zgodnie ze zdefiniowanym przez HIDL typu kolejki wiadomości).
  • Struktura MemTransaction zawiera 2 elementy: MemRegion struct, first i second jako odczyt lub zapis w bufor dzwonka może wymagać opuszczenia kolejki na początku kolejki. Ten oznaczałoby, że do odczytu/zapisu danych w FMQ potrzebne są dwa wskaźniki bazowe bufor dzwonka.

Aby uzyskać adres podstawowy i jego długość z struktury 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

Aby uzyskać odniesienia do pierwszego i drugiego elementu MemRegion w Obiekt MemTransaction:

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

Przykład zapisu do FMQ z użyciem interfejsów API typu zero-copy:

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
}

W MemTransaction są też dostępne te metody pomocnicze:

  • T* getSlot(size_t idx);
    Zwraca wskaźnik do boksu idx w ciągu znaków MemRegions, które są częścią tego komponentu (MemTransaction) obiektu. Jeśli obiekt MemTransaction reprezentuje pamięć regionów do odczytu/zapisu N elementów typu T, a następnie poprawny zakres idx mieści się w zakresie od 0 do N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    Zapisz nMessages elementy typu T w regionach pamięci opisane przez ten obiekt, począwszy od indeksu startIdx. Ta metoda ma format memcpy() i nie powinien być używany do wykonywania kopii zerowej . Jeśli obiekt MemTransaction reprezentuje pamięć do odczytu/zapisu N elementów typu T, prawidłowy zakres idx wynosi od 0 do N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    Metoda pomocnicza do odczytu nMessages elementów typu T z regiony pamięci opisane przez obiekt, począwszy od startIdx. Ten korzysta z metody memcpy() i nie powinna być używana do wykonywania kopii zerowej .

Wyślij kolejkę przez HIDL

Strona tworzenia:

  1. Utwórz obiekt kolejki wiadomości w sposób opisany powyżej.
  2. Sprawdź, czy obiekt jest prawidłowy w isValid().
  3. Jeśli czekasz na wiele kolejek, przechodząc EventFlag na długą formę readBlocking()/writeBlocking(), możesz wyodrębnić parametr wskaźnik flagi zdarzenia (za pomocą getEventFlagWord()) z MessageQueue obiekt, który został zainicjowany w celu utworzenia flagi, oraz użyj tej flagi do utworzenia niezbędnego obiektu EventFlag.
  4. Użyj metody MessageQueue getDesc(), aby uzyskać do obiektu deskryptora.
  5. W pliku .hal podaj parametrowi metody fmq_sync lub fmq_unsync, gdzie T to odpowiedni typ zdefiniowany przez HIDL. Użyj tego pola, aby wysłać obiekt zwrócony przez getDesc() do procesu odbierania.

Po stronie odbiorcy:

  1. Obiekt deskryptora pozwala utworzyć obiekt MessageQueue. Bądź użyj tego samego rodzaju kolejki i typu danych. W przeciwnym razie szablon nie kompilować.
  2. Jeśli została wyodrębniona flaga zdarzenia, wyodrębnij ją z odpowiednich MessageQueue obiekt w procesie odbierania.
  3. Do przeniesienia danych użyj obiektu MessageQueue.