למי שזקוק לתמיכה ב-AIDL, כדאי להיכנס אל FMQ עם AIDL.
תשתית הקריאה להליך מרוחק (RPC) של HIDL משתמשת במנגנוני Binder, כלומר קריאות כוללות תקורה, דורשות פעולות ליבה (kernel) ועשויות להפעיל פעולת התזמון. עם זאת, במקרים שבהם צריך להעביר נתונים בין תהליכים עם פחות תקורה וללא מעורבות ליבה (kernel) – Fast Message Queue נעשה שימוש במערכת (FMQ).
FMQ יוצר תורי הודעות עם המאפיינים הרצויים.
אובייקט MQDescriptorSync
או MQDescriptorUnsync
יכול להיות
נשלחה בקריאת HIDL RPC ומשמשת את תהליך המקבל כדי לגשת אל
תור ההודעות.
התכונה 'תורים להודעות מהירות' נתמכת רק ב-C++ ובמכשירים עם Android מגרסה 8.0 ואילך.
סוגי תורים של הודעות
ב-Android יש תמיכה בשני סוגי תורים (שנקראים טעמים):
- תורים לא מסונכרנים יכולים לגלוש, והם יכולים לכלול הרבה קוראים; כל קורא חייב לקרוא את הנתונים בזמן או לאבד אותם.
- תורים מסונכרנים לא ניתנים לגלישה, והם יכולים לכלול רק קורא אחד.
לא ניתן להשתמש בשני סוגי התורים (הקראה מתור ריק) נכשל) ויכול להיות לו רק כותב אחד.
לא מסונכרן
תור לא מסונכרן מכיל רק מחבר אחד, אבל יכול להכיל כל מספר של לקוראים. יש מיקום אחד לכתיבה לתור; עם זאת, כל קורא לעקוב אחרי המיקום הבלתי תלוי שלו לקריאה.
כתיבה לתור תמיד מצליחה (לא מסומנת אם יש חריגות) כל עוד הם לא גדולים מהקיבולת שהוגדרה בתור (כותבים גדולים מהערך הקיבולת של 'הבאים בתור' נכשלת באופן מיידי). מאחר שלכל קורא יכול להיות קריאה שונה במקום להמתין שכל קורא יקרא כל פיסת נתונים, יכול לצאת מהתור בכל פעם שכותבים חדשים צריכים מקום.
הקוראים אחראים לאחזור נתונים לפני שהם יוצאים בתור. קריאה שמנסה לקרוא יותר נתונים מאלה שזמינים הפעולה נכשלת באופן מיידי (אם היא לא מובילה לחסימה) או ממתינה שמספיק נתונים יהיו זמינים (אם ). קריאה שמנסה לקרוא יותר נתונים מהקיבולת תמיד של תור נכשל באופן מיידי.
אם קורא לא מצליח לעקוב אחר הכותב, כך שכמות הנתונים שקורא זה נכתב ועדיין לא נקרא על ידי הקורא, גדול יותר מקיבולת התור, הקריאה הבאה לא מחזירה נתונים במקום זאת, הוא מאפס את קריאת הנתונים כדי שווה למיקום הכתיבה האחרון, ואז מחזירה כשל. אם הנתונים הזמינים לקריאה נבדקים לאחר גלישה, אבל לפני הקריאה הבאה, מציג יותר נתונים זמינים לקריאה מאשר קיבולת התור, מה שמציין אירעה חריגה. (אם התור גולש בין בדיקת הנתונים הזמינים) ומנסה לקרוא את הנתונים האלה, האינדיקציה היחידה לגלישה היא הקריאה נכשלה.)
סביר להניח שקוראים של תור לא מסונכרן לא רוצים לאפס את מצביעי הקריאה והכתיבה של התור. לכן, כשיוצרים את התור קוראי מתאר צריכים להשתמש בארגומנט '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
של האובייקטים בכל תהליך לשימוש עם מכשירי FMQ אחרים. לחלופין,
אפשר לאתחל EventFlag
אובייקטים עם כל אובייקט משותף מתאים
זיכרון.
באופן כללי, בכל תור צריך להשתמש רק באחד וחסימה של סרטונים ארוכים. זו לא שגיאה לערבב ביניהם, אבל כדאי להיזהר צריך תכנות כדי לקבל את התוצאה הרצויה.
סימון הזיכרון לקריאה בלבד
כברירת מחדל, לזיכרון המשותף יש הרשאות לקריאה ולכתיבה. עבור לא מסונכרן
תורים (kUnsynchronizedWrite
), ייתכן שהכותב ירצה להסיר את הרשאות הכתיבה לכולם
של הקוראים לפני שהוא מחלק את האובייקטים MQDescriptorUnsync
. כך אפשר לוודא שהשני
תהליכים לא יכולים לכתוב לתור, ומומלץ להגן מפני באגים או התנהגות לא תקינה
שהקוראים מעבדים.
אם הכותב רוצה שהקוראים יוכלו לאפס את התור בכל פעם שהם משתמשים
MQDescriptorUnsync
כדי ליצור את הצד הנקרא של התור, ואז אי אפשר לסמן את הזיכרון
לקריאה בלבד. זוהי התנהגות ברירת המחדל של ה-constructor של 'MessageQueue'. לכן, אם יש כבר
למשתמשים הקיימים בתור הזה, צריך לשנות את הקוד שלהם כדי ליצור את התור עם
resetPointer=false
- כותב: קריאה אל
ashmem_set_prot_region
באמצעות מתאר קובץ שלMQDescriptor
והאזור מוגדר לקריאה בלבד (PROT_READ
):int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
- Reader: יצירת תור הודעות באמצעות
resetPointer=false
ברירת המחדל היאtrue
):mFmq = new (std::nothrow) MessageQueue(mqDesc, false);
שימוש ב'תור ההודעות'
ה-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()
עד להשלמת הפעולה המבוקשת, או עד שהזמן הקצוב לתפוגה יסתיים (
המשמעות של הערך 0 timeOutNanos
היא אף פעם לא זמן קצוב לתפוגה.
פעולות חסימה מיושמות באמצעות מילה של סימון אירוע. כברירת מחדל,
כל תור יוצר ומשתמש במילת סימון משלו כדי לתמוך בצורה הקצרה
readBlocking()
וגם writeBlocking()
ייתכן כי
מספר תורים לשיתוף מילה אחת, כך שתהליך
קוראת לאחד מהתורים. מצביע למילת הדגל של האירוע בתור יכול להיות
שמתקבלת באמצעות קריאה אל getEventFlagWord()
, והמצביע הזה (או כל
אל המיקום המתאים בזיכרון) כדי ליצור
אובייקט EventFlag
שצריך להעביר לפורמט הארוך
readBlocking()
ו-writeBlocking()
עבור
לרשימת 'הבאים בתור'. readNotification
וגם writeNotification
הפרמטרים מציינים באילו ביטים בדגל האירוע יש להשתמש כדי לאותת קריאות
כותב בתור הזה. readNotification
והקבוצה
writeNotification
הן מסכות ביטים של 32 ביט.
readBlocking()
מחכה על writeNotification
הביטים.
אם הערך של הפרמטר הוא 0, הקריאה תמיד תיכשל. אם
הערך של readNotification
הוא 0, השיחה לא תיכשל, אבל
קריאה מוצלחת לא תגדיר קטעי התראות. בתור מסונכרן,
המשמעות היא שהקריאה התואמת ל-writeBlocking()
אף פעם לא מתעוררת אלא אם הביט מוגדר במקום אחר. בתור לא מסונכרן,
writeBlocking()
לא מחכה (עדיין יש להשתמש בו כדי להגדיר את
כתוב ביט של התראה), והוא מתאים לכך שפעולות קריאה לא יגדירו
של התראות. באופן דומה, writeblocking()
ייכשל אם
הערך readNotification
הוא 0, וכתיבה מוצלחת מגדירה את הערך שצוין
writeNotification
ביטים.
כדי להמתין בכמה תורים בבת אחת, צריך להשתמש באובייקט EventFlag
wait()
ממתינה לקבלת מסכת התראות.
השיטה wait()
מחזירה מילת סטטוס עם הביטים שגרמו
הגדרת ההתעוררות מידע זה ישמש לאחר מכן כדי לאמת שהתור התואם
מספיק מקום או נתונים לפעולת הכתיבה/קריאה הרצויה
ללא חסימה של write()
/read()
. כדי לבצע פעולת פרסום
התראה, שימוש בשיחה אחרתEventFlag
אמצעי תשלום wake()
. להגדרה של EventFlag
הפשטה,
system/libfmq/include/fmq/EventFlag.h
.
אפס פעולות העתקה
read
/write
/readBlocking
/writeBlocking()
ממשקי API לוקחים מצביע אל מאגר נתונים זמני של קלט/פלט כארגומנט ומשתמשים
memcpy()
שיחות פנימיות להעתקת נתונים בין
מאגר לצלצול FMQ. כדי לשפר את הביצועים, מערכת Android מגרסה 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
פועלות באותו אופן. - ה-methods
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 באמצעות ממשקי API של אפס העתקה:
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 באזורי הזיכרון שתואר על ידי האובייקט, החל מהאינדקס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
בצד היצירה:
- יוצרים אובייקט של תור הודעות כמו שמתואר למעלה.
- מוודאים שהאובייקט תקין באמצעות
isValid()
. - אם אתם ממתינים בתורים מרובים על ידי העברה של
EventFlag
בצורה הארוכהreadBlocking()
/writeBlocking()
, אפשר לחלץ את מצביע סימון אירוע (באמצעותgetEventFlagWord()
) מ- אובייקטMessageQueue
שאותחל כדי ליצור את הדגל, וגם משתמשים בדגל הזה כדי ליצור את אובייקטEventFlag
הדרוש. - משתמשים בשיטה
MessageQueue
getDesc()
כדי לקבל שמתאר את האובייקט. - בקובץ
.hal
, צריך לציין ל-method פרמטר מסוגfmq_sync
אוfmq_unsync
כאשרT
הוא סוג מתאים בהגדרת HIDL. משתמשים בפונקציה הזו כדי לשלוח את האובייקט שהוחזר על ידיgetDesc()
לתהליך המקבל.
בצד המקבל:
- כדי ליצור אובייקט
MessageQueue
, צריך להשתמש באובייקט המתאר. להיות חשוב להשתמש באותו סגנון ובאותו סוג נתונים בתור, או שהתבנית לא תצליח להדר. - אם חילצת דגל של אירוע, צריך לחלץ את הדגל מהמערך המתאים
אובייקט
MessageQueue
בתהליך המקבל. - כדי להעביר נתונים צריך להשתמש באובייקט
MessageQueue
.