زیرساخت فراخوانی رویهای از دور (RPC) در HIDL از سازوکارهای binder استفاده میکند، یعنی فراخوانیها سربار دارند، به عملیات هسته نیاز دارند، و میتوانند عملکرد زمانبند را راهاندازی کنند. بااینحال، در مواردی که دادهها باید بین فرایندها با سربار کمتر و بدون دخالت هسته منتقل شوند، از سیستم «صف پیام سریع» (FMQ) استفاده میشود.
FMQ صفهای پیام را با ویژگیهای موردنظر ایجاد میکند. میتوانید شیء MQDescriptorSync یا MQDescriptorUnsync را ازطریق تماس HIDL RPC ارسال کنید و فرایند دریافتکننده از این شیء برای دسترسی به صف پیام استفاده میکند.
انواع صف
Android از دو نوع صف (که بهعنوان طعم شناخته میشوند) پشتیبانی میکند:
- صفهای همگامسازینشده میتوانند سرریز شوند و خوانندگان زیادی داشته باشند؛ هر خواننده باید دادهها را بهموقع بخواند، درغیراینصورت آنها را ازدست میدهد.
- صفهای همگامسازیشده مجاز به سرریز شدن نیستند و فقط میتوانند یک خواننده داشته باشند.
هر دو نوع صف مجاز به سرریز شدن نیستند (خواندن از صف خالی ناموفق است) و فقط یک نویسنده میتوانند داشته باشند.
صفهای همگامسازینشده
صف غیرهمگامسازیشده فقط یک نویسنده دارد، اما میتواند هر تعداد خواننده داشته باشد. یک موقعیت نوشتن برای صف وجود دارد؛ بااینحال، هر خواننده موقعیت خواندن مستقل خود را پیگیری میکند.
نوشتن در صف همیشه موفقیتآمیز است (سرریز بررسی نمیشود) تا زمانی که بزرگتر از ظرفیت صف پیکربندیشده نباشند (نوشتن بزرگتر از ظرفیت صف بلافاصله ناموفق میشود). ازآنجاییکه هر خواننده ممکن است موقعیت خواندن متفاوتی داشته باشد، بهجای اینکه منتظر بمانیم تا هر خواننده هر بخش از دادهها را بخواند، هرگاه نوشتارهای جدید به فضا نیاز داشته باشند، دادهها از صف خارج میشوند.
خوانندگان مسئول بازیابی دادهها قبلاز اینکه از انتهای صف خارج شوند هستند. خواندن دادههایی که تلاش میکند دادههای بیشتری از آنچه دردسترس است بخواند یا بلافاصله (اگر غیرمسدودکننده باشد) ناموفق میشود یا منتظر میماند تا دادههای کافی دردسترس قرار گیرد (اگر مسدودکننده باشد). درخواست خواندنی که تلاش میکند دادههای بیشتری از ظرفیت صف بخواند، همیشه فوراً ناموفق میشود.
اگر خوانندهای نتواند با نویسنده همگام شود، بهطوریکه مقدار دادههای نوشتهشده و هنوز خواندهنشده توسط آن خواننده از ظرفیت صف فراتر رود، خواندن بعدی دادهای برنمیگرداند؛ درعوض، موقعیت خواندن خواننده را به موقعیت نوشتن بهعلاوه نصف ظرفیت بازنشانی میکند و سپس خطایی برمیگرداند. این کار باعث میشود نیمی از میانگیر برای خواندن دردسترس باشد و فضا برای نوشتنهای جدید رزرو شود تا از سرریز شدن فوری مجدد صف جلوگیری شود. اگر دادههای دردسترس برای خواندن پساز سرریز اما قبلاز خواندن بعدی بررسی شود، دادههای دردسترس بیشتری را برای خواندن نسبتبه ظرفیت صف نشان میدهد که نشاندهنده وقوع سرریز است. (اگر صف بین بررسی دادههای دردسترس و تلاش برای خواندن آن دادهها سرریز شود، تنها نشانه سرریز این است که خواندن ناموفق است.)
صفهای همگامسازیشده
صف همگامسازیشده یک نویسنده و یک خواننده با یک موقعیت نوشتن و یک موقعیت خواندن دارد. نوشتن دادههای بیشتر از فضای موجود در صف یا خواندن دادههای بیشتر از آنچه درحالحاضر در صف وجود دارد غیرممکن است. بسته به اینکه تابع نوشتن یا خواندن مسدودکننده یا غیرمسدودکننده فراخوانی شده باشد، تلاش برای فراتر رفتن از فضای دردسترس یا دادهها یا بلافاصله با شکست مواجه میشود یا تا زمانی که عملیات موردنظر تکمیل شود مسدود میشود. تلاش برای خواندن یا نوشتن دادههای بیشتر از ظرفیت صف همیشه فوراً ناموفق میشود.
راهاندازی 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 nonblocking 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 نشان میدهد که آیا هنگام ایجاد این شیء MessageQueue، موقعیتهای خواندن و نوشتن به ۰ بازنشانی شوند یا نه.
در صف غیرهمگامسازیشده، موقعیت خواندن (که برای هر
MessageQueue شیء در صفهای غیرهمگامسازیشده محلی است) همیشه درطول ایجاد روی ۰ تنظیم میشود. معمولاً 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() تماس ارائه دهید. میتوانید یکی از صفها را با پرچم رویداد داخلی مقداردهی اولیه کنید، که سپس باید بااستفاده از getEventFlagWord() از MessageQueue شیء آن صف استخراج شود و برای ایجاد EventFlag شیء در هر فرایند برای استفاده با FMQهای دیگر استفاده شود. یا میتوانید اشیای
EventFlag را با هر حافظه مشترک مناسبی مقداردهی اولیه کنید.
بهطورکلی، هر صف باید فقط از یکی از موارد غیرمسدودکننده، مسدودکننده کوتاه، یا مسدودکننده بلند استفاده کند. ترکیب آنها خطا نیست، اما برای رسیدن به نتیجه مطلوب، برنامهنویسی دقیق لازم است.
علامتگذاری حافظه بهعنوان فقط خواندنی
بهطور پیشفرض، حافظه مشترک اجازه خواندن و نوشتن دارد. برای صفهای غیرهمگامسازیشده (kUnsynchronizedWrite)، ممکن است نویسنده بخواهد قبلاز توزیع اشیای MQDescriptorUnsync، اجازههای نوشتن را برای همه خوانندگان بردارد. این کار تضمین میکند که فرایندهای دیگر نتوانند در صف بنویسند، که برای محافظت دربرابر اشکالات یا رفتار بد در فرایندهای خواننده توصیه میشود.
اگر نویسنده میخواهد خوانندگان بتوانند هر زمان که از
MQDescriptorUnsync برای ایجاد طرف خواندن صف استفاده میکنند، صف را بازنشانی کنند، حافظه نمیتواند بهعنوان
فقط خواندنی علامتگذاری شود. این رفتار پیشفرض سازنده MessageQueue است. بنابراین اگر
کاربران فعلی برای این صف وجود داشته باشند، کد آنها باید تغییر کند تا صف با
resetPointer=false ساخته شود.
- نویسنده: با توصیفگر فایل
MQDescriptorباashmem_set_prot_regionتماس بگیرید و منطقه را روی فقط خواندنی (PROT_READ) تنظیم کنید:int res = ashmem_set_prot_region(mqDesc->handle->data[0], PROT_READ)
- Reader: Create message queue with
resetPointer=false(the default istrue):mFmq = new (std::nothrow) MessageQueue(mqDesc, false);
استفاده از MessageQueue
میانای برنامهسازی کاربردی عمومی شیء 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 صفر یعنی زمان آنها هرگز بهپایان نمیرسد).
عملیات مسدودسازی بااستفاده از کلمه پرچم رویداد اجرا میشود. بهطور پیشفرض، هر صف کلمه پرچم خود را برای پشتیبانی از شکل کوتاه readBlocking() و writeBlocking() ایجاد و استفاده میکند. چندین صف میتوانند یک کلمه را بهاشتراک بگذارند، بنابراین یک فرایند میتواند برای نوشتن یا خواندن در هریک از صفها منتظر بماند. با فراخوانی getEventFlagWord()، میتوانید اشارهگری به
کلمه پرچم رویداد صف دریافت کنید، و میتوانید از آن اشارهگر (یا هر
اشارهگر به مکان حافظه مشترک مناسب) برای ایجاد
شیء EventFlag استفاده کنید تا به شکل طولانی
readBlocking() و writeBlocking() برای صف
دیگری منتقل شود. پارامترهای readNotification و writeNotification
مشخص میکنند که از کدام بیتهای پرچم رویداد باید برای نشان دادن خواندن و
نوشتن در آن صف استفاده شود. readNotification و
writeNotification ماسکهای بیتی ۳۲ بیتی هستند.
readBlocking() در writeNotification بیت منتظر میماند؛
اگر این پارامتر ۰ باشد، تماس همیشه ناموفق خواهد بود. اگر مقدار
readNotification صفر باشد، تماس ناموفق نخواهد بود، اما
خواندن موفق هیچیک از بیتهای اعلان را تنظیم نخواهد کرد. در صف همزمانسازیشده،
این یعنی تماس writeBlocking() مربوطه
هرگز بیدار نمیشود، مگراینکه بیت در جای دیگری تنظیم شده باشد. در صف غیرهمگامسازیشده،
writeBlocking() منتظر نمیماند (همچنان باید برای تنظیم
بیت اعلان نوشتن استفاده شود)، و مناسب است که خواندنها هیچ
بیت اعلانی را تنظیم نکنند. بههمین ترتیب، اگر readNotification صفر باشد، writeblocking() ناموفق خواهد بود، و نوشتن موفقیتآمیز بیتهای writeNotification مشخصشده را تنظیم میکند.
برای انتظار در چند صف بهطور همزمان، از روش EventFlag شیء
wait() برای انتظار در یک ماسک بیت اعلانها استفاده کنید. روش
wait() کلمه وضعیت را با بیتهایی که باعث بیدار شدن شدهاند برمیگرداند. سپس از این اطلاعات برای درستیسنجی اینکه صف مربوطه فضای کافی یا دادههای کافی برای عملیات نوشتن و خواندن موردنظر دارد استفاده میشود و write() و read() غیرمسدودکننده اجرا میشود. برای دریافت اعلان عملیات پست، از تماس دیگری با روش wake() شیء EventFlag استفاده کنید. برای تعریف EventFlag
انتزاع، به
system/libfmq/include/fmq/EventFlag.h مراجعه کنید.
عملیات کپی صفر
روشهای
read، write، readBlocking، و writeBlocking()
اشارهگری به میانگیر ورودی-خروجی را بهعنوان آرگومان میگیرند و از
فراخوانیهای memcpy() بهصورت داخلی برای کپی کردن دادهها بین همان میانگیر و
میانگیر حلقهای FMQ استفاده میکنند. برای بهبود عملکرد، Android 8.0 و نسخههای بالاتر شامل مجموعهای از
واسطهای برنامهنویسی کاربردی است که دسترسی مستقیم به اشارهگر در بافر حلقه را فراهم میکند و
نیاز به استفاده از 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تعداد پیامهایی را که باید خوانده و نوشته شود بهعنوان ورودی میگیرند و مقدار بولی برمیگردانند که نشان میدهد خواندن یا نوشتن امکانپذیر است یا نه. اگر خواندن یا نوشتن امکانپذیر باشد،memTxstruct با اشارهگرهای پایهای پر میشود که میتوان از آنها برای دسترسی مستقیم اشارهگر به حافظه مشترک میانگیر حلقهای استفاده کرد. - ساختار
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 بااستفاده از میاناهای برنامهسازی کاربردی کپی صفر:
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بین ۰ و N-1 است. -
bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1);nMessagesمورد از نوعTرا در ناحیههای حافظه شرحدادهشده توسط شیء، از نمایهstartIdxمینویسد. این روش ازmemcpy()استفاده میکند و برای عملیات بدون کپی درنظر گرفته نشده است. اگر شیءMemTransactionنشاندهنده حافظه برای خواندن و نوشتن N مورد از نوعTباشد، محدوده معتبرidxبین ۰ و N-۱ است. -
bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1);روش کمکی برای خواندنnMessagesمورد از نوعTاز مناطق حافظه توصیفشده توسط شیء شروع ازstartIdxاست. این روش ازmemcpy()استفاده میکند و برای استفاده در عملیات کپی صفر درنظر گرفته نشده است.
ارسال صف ازطریق HIDL
در طرف ایجادکننده:
- همانطور که در بالا توضیح داده شد، یک شیء صف پیام ایجاد کنید.
- با
isValid()درستیسنجی کنید که شیء معتبر باشد. - اگر با گذراندن
EventFlagبه شکل طولانیreadBlocking()یاwriteBlocking()در چندین صف منتظر هستید، میتوانید اشارهگر پرچم رویداد (بااستفاده ازgetEventFlagWord()) را از شیءMessageQueueکه برای ایجاد پرچم مقداردهی اولیه شده است استخراج کنید و از آن پرچم برای ایجاد شیءEventFlagلازم استفاده کنید. - از روش
MessageQueuegetDesc()برای دریافت شیء توصیفگر استفاده کنید. - در فایل HAL، به این روش پارامتری از نوع
fmq_syncیاfmq_unsyncبدهید که در آنTنوع مناسب تعریفشده با HIDL است. از این برای ارسال شیء برگشتی ازgetDesc()به فرایند دریافت استفاده کنید.
در طرف دریافتکننده:
- از شیء توصیفکننده برای ایجاد شیء
MessageQueueاستفاده کنید. از طعم صف و نوع داده یکسانی استفاده کنید، درغیراینصورت الگو کامپایل نمیشود. - اگر پرچم رویدادی را استخراج کردید، پرچم را از شیء
MessageQueueمربوطه در فرایند دریافت استخراج کنید. - برای انتقال دادهها از شیء
MessageQueueاستفاده کنید.