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

אם אתה מחפש תמיכה ב-AIDL, ראה גם FMQ עם AIDL .

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

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

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

סוגי MessageQueue

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

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

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

לא מסונכרן

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

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

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

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

סביר שקוראים של תור לא מסונכרן לא ירצו לאפס את מצביעי הקריאה והכתיבה של התור. לכן, בעת יצירת התור מהמתאר, הקוראים צריכים להשתמש בארגומנט `false` עבור הפרמטר `resetPointers`.

מסונכרן

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

הגדרת 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 או AIDL 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 */);
    
  • צורה ארוכה , עם שישה פרמטרים (כולל דגל אירוע ומסיכות סיביות). תומך בשימוש באובייקט EventFlag משותף בין תורים מרובים ומאפשר לציין את מסכות סיביות ההתראה לשימוש. במקרה זה, יש לספק את דגל האירוע ומסיכות הסיביות לכל קריאת קריאה וכתיבה.

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

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

סימון הזיכרון כקריאה בלבד

כברירת מחדל, לזיכרון המשותף יש הרשאות קריאה וכתיבה. עבור תורים לא מסונכרנים ( kUnsynchronizedWrite ), ייתכן שהכותב ירצה להסיר הרשאות כתיבה עבור כל הקוראים לפני שהוא מחלק את האובייקטים MQDescriptorUnsync . זה מבטיח שהתהליכים האחרים לא יכולים לכתוב לתור, מה שמומלץ להגן מפני באגים או התנהגות רעה בתהליכי הקורא. אם הכותב רוצה שהקוראים יוכלו לאפס את התור בכל פעם שהם משתמשים ב- MQDescriptorUnsync כדי ליצור את צד הקריאה של התור, אז לא ניתן לסמן את הזיכרון כקריאה בלבד. זוהי התנהגות ברירת המחדל של הבנאי 'MessageQueue'. לכן, אם יש כבר משתמשים קיימים בתור הזה, יש לשנות את הקוד שלהם כדי לבנות את התור עם resetPointer=false .

  • כותב: התקשר ל- ashmem_set_prot_region עם מתאר קובץ MQDescriptor והאזור מוגדר לקריאה בלבד ( PROT_READ ):
    int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
  • קורא: צור תור הודעות עם resetPointer=false (ברירת המחדל היא true ):
    mFmq = new (std::nothrow) MessageQueue(mqDesc, false);

שימוש ב-MessageQueue

ה-API הציבורי של האובייקט 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);

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