תור הודעות מהיר (FMQ)

קל לארגן דפים בעזרת אוספים אפשר לשמור ולסווג תוכן על סמך ההעדפות שלך.

תשתית Call Procedure מרחוק (RPC) של HIDL משתמשת במנגנוני Binder, כלומר שיחות כרוכות בתקורה, דורשות פעולות ליבה ועשויות להפעיל פעולת מתזמן. עם זאת, במקרים בהם יש להעביר נתונים בין תהליכים עם פחות תקורה וללא מעורבות ליבה, נעשה שימוש במערכת ה-Fast Message Queue (FMQ).

FMQ יוצר תורי הודעות עם המאפיינים הרצויים. ניתן לשלוח אובייקט MQDescriptorSync או MQDescriptorUnsync באמצעות קריאת HIDL RPC ולהשתמש בתהליך הקבלה כדי לגשת לתור ההודעות.

תורי הודעות מהירים נתמכים רק ב-C++ ובמכשירים עם אנדרואיד 8.0 ומעלה.

סוגי MessageQueue

אנדרואיד תומך בשני סוגי תורים (הידועים כטעמים ):

  • תורים לא מסונכרנים מורשים לעלות על גדותיו, ויכולים להיות להם קוראים רבים; כל קורא חייב לקרוא נתונים בזמן או לאבד אותם.
  • תורים מסונכרנים אינם רשאים לעלות על גדותיהם, ויכולים להיות להם קורא אחד בלבד.

שני סוגי התורים אינם רשאים להסתער (קריאה מתוך תור ריק תיכשל) ויכולים להיות בעלי כותב אחד בלבד.

לא מסונכרן

לתור לא מסונכרן יש כותב אחד בלבד, אבל יכול להיות כל מספר של קוראים. יש עמדת כתיבה אחת לתור; עם זאת, כל קורא עוקב אחר עמדת הקריאה העצמאית שלו.

כתיבה לתור תמיד מצליחה (לא נבדקת עבור גלישה) כל עוד הן אינן גדולות מקיבולת התור שהוגדרה (כתיבה גדולה מקיבולת התור נכשלת מיד). מכיוון שלכל קורא יתכן מיקום קריאה שונה, במקום לחכות שכל קורא יקרא כל פיסת נתונים, הנתונים רשאים ליפול מהתור בכל פעם שכתובות חדשות זקוקות למרחב.

קריאות אחראיות לאחזור נתונים לפני שהם נופלים מקצה התור. קריאה שמנסה לקרוא יותר נתונים ממה שזמינים נכשלת מיד (אם לא חוסמת) או ממתינה לדי נתונים זמינים (אם חוסמים). קריאה שמנסה לקרוא יותר נתונים מקיבולת התור תמיד נכשלת מיד.

אם קורא לא מצליח לעמוד בקצב של הכותב, כך שכמות הנתונים שנכתבו ועדיין לא נקראו על ידי אותו קורא גדולה מיכולת התור, הקריאה הבאה לא מחזירה נתונים; במקום זאת, הוא מאפס את מיקום הקריאה של הקורא כך שיהיה שווה למיקום הכתיבה האחרון ואז מחזיר כשל. אם הנתונים הזמינים לקריאה נבדקים לאחר הצפה אך לפני הקריאה הבאה, הוא מציג יותר נתונים זמינים לקריאה מאשר קיבולת התור, מה שמצביע על התרחשה הצפה. (אם התור עולה על גדותיו בין בדיקת נתונים זמינים לניסיון לקרוא נתונים אלה, האינדיקציה היחידה לגלישה היא שהקריאה נכשלת).

מסונכרן

לתור מסונכרן יש כותב אחד וקורא אחד עם עמדת כתיבה בודדת ועמדת קריאה אחת. אי אפשר לכתוב יותר נתונים ממה שיש לתור מקום או לקרוא יותר נתונים ממה שהתור מכיל כרגע. תלוי אם נקראת פונקציית הכתיבה או הקריאה החוסמת או שאינה חוסמת, ניסיונות לחרוג מהשטח הפנוי או מהנתונים הפנויים או מחזירים כשל מיד או חסימה עד שניתן להשלים את הפעולה הרצויה. ניסיונות לקרוא או לכתוב יותר נתונים מקיבולת התור תמיד ייכשלו מיד.

הגדרת FMQ

תור הודעות דורש מספר אובייקטים של MessageQueue : אחד לכתיבה ואחד או יותר לקריאתו. אין תצורה מפורשת של איזה אובייקט משמש לכתיבה או קריאה; זה תלוי במשתמש לוודא ששום אובייקט לא משמש הן לקריאה והן לכתיבה, שיש לכל היותר כותב אחד, ולגבי תורים מסונכרנים, שיש קורא אחד לכל היותר.

יצירת האובייקט הראשון של MessageQueue

תור הודעות נוצר ומוגדר בשיחה אחת:

#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 */);
  • MessageQueue<T, flavor>(numElements) יוצר ומאתחל אובייקט התומך בפונקציונליות של תור ההודעות.
  • MessageQueue<T, flavor>(numElements, configureEventFlagWord) יוצר ומאתחל אובייקט התומך בפונקציונליות של תור ההודעות עם חסימה.
  • flavor יכול להיות kSynchronizedReadWrite עבור תור מסונכרן או kUnsynchronizedWrite עבור תור לא מסונכרן.
  • uint16_t (בדוגמה זו) יכול להיות כל סוג המוגדר ב-HIDL שאינו כולל מאגרים מקוננים (ללא סוגי string או vec ), ידיות או ממשקים.
  • kNumElementsInQueue מציין את גודל התור במספר הכניסות; הוא קובע את גודל מאגר הזיכרון המשותף שיוקצה לתור.

יצירת האובייקט השני של MessageQueue

הצד השני של תור ההודעות נוצר באמצעות אובייקט MQDescriptor המתקבל מהצד הראשון. האובייקט MQDescriptor נשלח באמצעות קריאת HIDL RPC לתהליך שיחזיק את הקצה השני של תור ההודעות. ה- MQDescriptor מכיל מידע על התור, כולל:

  • מידע למיפוי המאגר ומצביע הכתיבה.
  • מידע למיפוי מצביע הקריאה (אם התור מסונכרן).
  • מידע למיפוי מילת דגל האירוע (אם התור חוסם).
  • סוג אובייקט ( <T, flavor> ), הכולל את הסוג המוגדר ב-HIDL של רכיבי תור וטעם התור (מסונכרן או לא מסונכרן).

ניתן להשתמש באובייקט MQDescriptor לבניית אובייקט MessageQueue :

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

הפרמטר resetPointers מציין אם לאפס את עמדות הקריאה והכתיבה ל-0 בעת יצירת אובייקט MessageQueue זה. בתור לא מסונכרן, מיקום הקריאה (המקומי לכל אובייקט MessageQueue בתורים לא מסונכרנים) מוגדר תמיד ל-0 במהלך היצירה. בדרך כלל, ה- MQDescriptor מאותחל במהלך יצירת אובייקט תור ההודעות הראשון. לשליטה נוספת על הזיכרון המשותף, אתה יכול להגדיר את ה- MQDescriptor באופן ידני ( MQDescriptor מוגדר ב- system/libhidl/base/include/hidl/MQDescriptor.h ) ואז ליצור כל אובייקט MessageQueue כמתואר בסעיף זה.

חסימת תורים ודגלי אירועים

כברירת מחדל, התורים אינם תומכים בחסימת קריאה/כתיבה. ישנם שני סוגים של חסימת שיחות קריאה/כתיבה:

  • טופס קצר , עם שלושה פרמטרים (מצביע נתונים, מספר פריטים, פסק זמן). תומך בחסימה בפעולות קריאה/כתיבה בודדות בתור בודד. בעת שימוש בטופס זה, התור יטפל בדגל האירוע ובמסכות הסיביות באופן פנימי, ויש לאתחל את אובייקט תור ההודעות הראשון עם פרמטר שני של true . לדוגמה:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
    
  • Long form, with six parameters (includes event flag and bitmasks). Supports using a shared EventFlag object between multiple queues and allows specifying the notification bit masks to be used. In this case, the event flag and bitmasks must be supplied to each read and write call.

For the long form, the EventFlag can be supplied explicitly in each readBlocking() and writeBlocking() call. One of the queues may be initialized with an internal event flag, which must then be extracted from that queue's MessageQueue objects using getEventFlagWord() and used to create EventFlag objects in each process for use with other FMQs. Alternatively, the EventFlag objects can be initialized with any suitable shared memory.

In general, each queue should use only one of non-blocking, short-form blocking, or long-form blocking. It is not an error to mix them, but careful programming is required to get the desired result.

Using the MessageQueue

The public API of the MessageQueue object is:

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() וב- availableToRead() כדי לקבוע כמה נתונים ניתן להעביר בפעולה אחת. בתור לא מסונכרן:

  • availableToWrite() תמיד מחזיר את הקיבולת של התור.
  • לכל קורא יש מיקום קריאה משלו והוא עושה חישוב משלו עבור availableToRead() .
  • מנקודת מבטו של קורא איטי, התור רשאי לעלות על גדותיו; זה עלול לגרום ל- availableToRead() להחזיר ערך גדול מגודל התור. הקריאה הראשונה לאחר הצפה תיכשל ותגרום לכך שמיקום הקריאה עבור אותו קורא יוגדר שווה למצביע הכתיבה הנוכחי, בין אם הגלישה דווחה דרך availableToRead() ובין אם לאו.

המתודות read() ו- write() מחזירות true אם ניתן (והו) להעביר את כל הנתונים המבוקשים אל/מן התור. שיטות אלו אינן חוסמות; או שהם מצליחים (ומחזירים true ), או מחזירים כישלון ( false ) מיד.

readBlocking() ו- writeBlocking() ממתינות עד שניתן יהיה להשלים את הפעולה המבוקשת, או עד שהן קצוב לזמן קצוב (ערך timeOutNanos של 0 פירושו שלעולם לא פסק זמן).

פעולות חסימה מיושמות באמצעות מילת דגל אירוע. כברירת מחדל, כל תור יוצר ומשתמש במילת דגל משלו כדי לתמוך בצורה הקצרה של readBlocking() ו- writeBlocking() . יש אפשרות למספר תורים לשתף מילה בודדת, כך שתהליך יכול להמתין לכתיבה או קריאה לכל אחד מהתורים. ניתן להשיג מצביע למילת דגל אירוע של תור על ידי קריאה getEventFlagWord() , וניתן להשתמש במצביע הזה (או כל מצביע למיקום זיכרון משותף מתאים) ליצירת אובייקט EventFlag שיעבור לצורה הארוכה של readBlocking() ו writeBlocking() עבור תור אחר. הפרמטרים readNotification ו- writeNotification אומרים באילו סיביות בדגל האירוע יש להשתמש לאותת קריאה וכתיבה בתור. readNotification ו- writeNotification הם מסכות סיביות של 32 סיביות.

readBlocking() מחכה על סיביות writeNotification ; אם הפרמטר הזה הוא 0, השיחה תמיד נכשלת. אם ערך readNotification הוא 0, השיחה לא תיכשל, אך קריאה מוצלחת לא תגדיר סיביות התראה. בתור מסונכרן, המשמעות היא שהקריאה המתאימה writeBlocking() לעולם לא תתעורר אלא אם כן הביט מוגדר במקום אחר. בתור לא מסונכרן, writeBlocking() לא ימתין (עדיין יש להשתמש בו כדי להגדיר את סיביות הכתיבה), ומתאים לקריאה לא להגדיר סיביות התראה. באופן דומה, writeblocking() ייכשל אם readNotification הוא 0, וכתיבה מוצלחת מגדירה את סיביות writeNotification שצוינו.

כדי להמתין על מספר תורים בו-זמנית, השתמש בשיטת wait() של אובייקט EventFlag כדי להמתין למסכת סיביות של התראות. השיטה wait() מחזירה מילת סטטוס עם הביטים שגרמו ל-Wike up set. מידע זה משמש לאחר מכן כדי לוודא שלתור המתאים יש מספיק מקום או נתונים עבור פעולת הכתיבה/קריאה הרצויה ולבצע write() / read() . כדי לקבל הודעה לאחר הפעולה, השתמש בקריאה אחרת לשיטת wake() של EventFlag . להגדרה של הפשטה של EventFlag , עיין ב- system/libfmq/include/fmq/EventFlag.h .

אפס פעולות העתקה

ממשקי ה-API של read / write / readBlocking / writeBlocking() לוקחים מצביע למאגר קלט/פלט כארגומנט ומשתמשים בקריאות memcpy() באופן פנימי כדי להעתיק נתונים בין אותו למאגר הצלצול של FMQ. כדי לשפר את הביצועים, אנדרואיד 8.0 ואילך כולל קבוצה של ממשקי API המספקים גישה ישירה למצביע למאגר הצלצול, ומבטלים את הצורך להשתמש בשיחות memcpy .

השתמש בממשקי ה-API הציבוריים הבאים עבור פעולות FMQ אפס עותק:

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);
  • שיטת beginWrite מספקת מצביעי בסיס למאגר צלצול FMQ. לאחר כתיבת הנתונים, בצע אותם באמצעות commitWrite() . שיטות beginRead / commitRead פועלות באותה צורה.
  • שיטות beginRead / Write לוקחות כקלט את מספר ההודעות שיש לקרוא/לכתוב ומחזירות ערך בוליאני המציין אם הקריאה/כתיבה אפשרית. אם הקריאה או הכתיבה אפשריים, מבנה memTx מאוכלס במצביעי בסיס שניתן להשתמש בהם לגישה ישירה למצביע לתוך הזיכרון המשותף של מאגר הטבעת.
  • המבנה של MemRegion מכיל פרטים על בלוק זיכרון, כולל מצביע הבסיס (כתובת הבסיס של בלוק הזיכרון) והאורך במונחים של T (אורך בלוק הזיכרון במונחים של הסוג המוגדר ב-HIDL של תור ההודעות).
  • מבנה MemTransaction מכיל שני MemRegion , first second כקריאה או כתיבה במאגר הטבעת עשויה לדרוש גלישה לתחילת התור. המשמעות היא ששני מצביעי בסיס נחוצים כדי לקרוא/לכתוב נתונים לתוך מאגר הצלצול של FMQ.

כדי לקבל את כתובת הבסיס והאורך 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

כדי לקבל הפניות ל- MemRegion הראשון והשני בתוך אובייקט MemTransaction :

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

דוגמה לכתיבה ל-FMQ באמצעות 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
}

שיטות העזר הבאות הן גם חלק מ- MemTransaction :

  • T* getSlot(size_t idx);
    מחזיר מצביע לחריץ idx בתוך MemRegions שהם חלק מאובייקט MemTransaction זה. אם האובייקט MemTransaction מייצג את אזורי הזיכרון לקריאה/כתיבה של N פריטים מסוג T, אז הטווח החוקי של idx הוא בין 0 ל-N-1.
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);
    כתוב פריטי nMessages מסוג T לתוך אזורי הזיכרון המתוארים על ידי האובייקט, החל מ- index startIdx . שיטה זו משתמשת ב- memcpy() ואינה מיועדת לשימוש עבור פעולת העתקה אפס. אם האובייקט MemTransaction מייצג זיכרון לקריאה/כתיבה של N פריטים מסוג T, אז הטווח החוקי של idx הוא בין 0 ל-N-1.
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);
    שיטת עוזר לקריאת פריטי nMessages מסוג T מאזורי הזיכרון המתוארים על ידי האובייקט החל מ- startIdx . שיטה זו משתמשת ב- memcpy() ואינה מיועדת לשימוש עבור פעולת העתקה אפס.

שליחת התור דרך HIDL

בצד היצירה:

  1. צור אובייקט בתור הודעות כמתואר לעיל.
  2. ודא שהאובייקט חוקי באמצעות isValid() .
  3. אם תמתין לתורים מרובים על ידי העברת EventFlag לצורה הארוכה של readBlocking() / writeBlocking() , תוכל לחלץ את מצביע דגל האירוע (באמצעות getEventFlagWord() ) מאובייקט MessageQueue שאותחל ליצירת הדגל, והשתמש בדגל הזה כדי ליצור את האובייקט הנחוץ EventFlag .
  4. השתמש בשיטת MessageQueue getDesc() כדי לקבל אובייקט מתאר.
  5. בקובץ .hal , תן למתודה פרמטר מסוג fmq_sync או fmq_unsync כאשר T הוא סוג מתאים המוגדר HIDL. השתמש בזה כדי לשלוח את האובייקט שהוחזר על ידי getDesc() לתהליך הקבלה.

בצד המקבל:

  1. השתמש באובייקט המתאר כדי ליצור אובייקט MessageQueue . הקפד להשתמש באותו טעם תור וסוג נתונים, אחרת התבנית לא תצליח להדר.
  2. אם חילצתם דגל אירוע, חלץ את הדגל מאובייקט MessageQueue המתאים בתהליך הקבלה.
  3. השתמש באובייקט MessageQueue כדי להעביר נתונים.