صف پیام سریع (FMQ)

زیرساخت فراخوانی روش راه دور (RPC) HIDL از مکانیسم‌های بایندر استفاده می‌کند، به این معنی که تماس‌ها شامل سربار هستند، به عملیات هسته نیاز دارند و می‌توانند عملیات زمان‌بندی را آغاز کنند. با این حال، برای مواردی که داده ها باید بین فرآیندهایی با سربار کمتر و بدون دخالت هسته منتقل شوند، از سیستم صف پیام سریع (FMQ) استفاده می شود.

FMQ صف های پیام را با ویژگی های مورد نظر ایجاد می کند. شما می توانید یک شی MQDescriptorSync یا MQDescriptorUnsync را از طریق یک تماس HIDL RPC ارسال کنید و شی توسط فرآیند دریافت برای دسترسی به صف پیام استفاده می شود.

انواع صف

اندروید از دو نوع صف (معروف به طعم ها ) پشتیبانی می کند:

  • صف های غیرهمگام مجاز به سرریز شدن هستند و می توانند خواننده های زیادی داشته باشند. هر خواننده باید داده ها را به موقع بخواند یا آن را از دست بدهد.
  • صف های همگام سازی شده مجاز به سرریز شدن نیستند و فقط می توانند یک خواننده داشته باشند.

هر دو نوع صف مجاز به پایین آمدن نیستند (خواندن از یک صف خالی با شکست مواجه می شود) و می توانند فقط یک نویسنده داشته باشند.

صف های غیر همگام

یک صف غیرهمگام فقط یک نویسنده دارد، اما می تواند هر تعداد خواننده داشته باشد. یک موقعیت نوشتن برای صف وجود دارد. با این حال، هر خواننده موقعیت خواندن مستقل خود را پیگیری می کند.

نوشتن در صف همیشه با موفقیت انجام می شود (برای سرریز بررسی نمی شود) تا زمانی که بزرگتر از ظرفیت صف پیکربندی شده نباشد (بزرگتر از ظرفیت صف می نویسد بلافاصله با شکست مواجه می شود). از آنجایی که هر خواننده ممکن است موقعیت خواندن متفاوتی داشته باشد، به جای اینکه منتظر هر خواننده برای خواندن هر قطعه داده باشد، هر زمان که نوشته های جدید به فضا نیاز داشته باشند، داده ها از صف خارج می شوند.

خوانندگان مسئول بازیابی داده ها قبل از اینکه از انتهای صف بیفتند، هستند. خواندنی که تلاش می‌کند داده‌های بیشتری از موجود را بخواند یا فوراً با شکست مواجه می‌شود (اگر مسدود نباشد) یا منتظر می‌ماند تا داده‌های کافی در دسترس باشد (در صورت مسدود شدن). خواندنی که تلاش می‌کند داده‌های بیشتری از ظرفیت صف را بخواند، همیشه بلافاصله با شکست مواجه می‌شود.

اگر خواننده نتواند با رایتر همگام شود، به طوری که مقدار داده های نوشته شده و هنوز خوانده نشده توسط آن خواننده بیشتر از ظرفیت صف باشد، خواندن بعدی داده را بر نمی گرداند. در عوض، موقعیت خواندن خواننده را بازنشانی می کند تا با آخرین موقعیت نوشتن برابری کند و سپس شکست را برمی گرداند. اگر داده‌های موجود برای خواندن پس از سرریز بررسی شود، اما قبل از خواندن بعدی، داده‌های موجود برای خواندن بیشتر از ظرفیت صف را نشان می‌دهد، که نشان می‌دهد سرریز رخ داده است. (اگر صف بین بررسی داده های موجود و تلاش برای خواندن آن داده ها سرریز شود، تنها نشانه سرریز این است که خواندن ناموفق است.)

صف های هماهنگ شده

یک صف هماهنگ دارای یک نویسنده و یک خواننده با یک موقعیت نوشتن و یک موقعیت خواندن واحد است. غیرممکن است که اطلاعات بیشتری نسبت به صف موجود در صف بنویسید یا اطلاعات بیشتری را نسبت به صف در حال حاضر بخوانید. بسته به اینکه تابع نوشتن یا خواندن مسدود یا غیرمسدود فراخوانی شود، تلاش می‌شود از فضای موجود یا داده‌ها فراتر رود، یا فوراً شکست را برمی‌گرداند یا تا زمانی که عملیات مورد نظر تکمیل شود، مسدود می‌شود. تلاش برای خواندن یا نوشتن داده های بیشتر از ظرفیت صف همیشه بلافاصله با شکست مواجه می شود.

یک 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 ، موقعیت های خواندن و نوشتن به 0 بازنشانی شود یا خیر. در یک صف غیرهمگام، موقعیت خواندن (که محلی برای هر شیء 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 برای ایجاد سمت خواندنی صف استفاده می کنند، صف را بازنشانی کنند، آنگاه نمی توان حافظه را به عنوان فقط خواندنی علامت گذاری کرد. این رفتار پیش فرض سازنده MessageQueue است. بنابراین اگر کاربران موجود از این صف وجود دارند، کد آنها باید تغییر کند تا صف با resetPointer=false ساخته شود.

  • Writer: 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);

از 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() هرگز بیدار نمی شود مگر اینکه بیت در جای دیگری تنظیم شده باشد. در یک صف غیر همگام، writeBlocking() منتظر نمی ماند (هنوز باید برای تنظیم بیت اعلان نوشتن استفاده شود)، و مناسب است برای خواندن هیچ بیت اعلان تنظیم نشود. به طور مشابه، در صورتی که readNotification 0 باشد، writeblocking() با شکست مواجه می شود و یک نوشتن موفق بیت های writeNotification مشخص شده را تنظیم می کند.

برای منتظر ماندن روی چندین صف به طور همزمان، از روش wait() یک شیء EventFlag استفاده کنید تا روی یک بیت ماسک از اعلان ها منتظر بمانید. متد wait() یک کلمه وضعیت را با بیت هایی که باعث تنظیم بیدار شدن شده اند برمی گرداند. سپس از این اطلاعات برای تأیید اینکه صف مربوطه دارای فضای کافی یا داده برای عملیات نوشتن و خواندن مورد نظر است و اجرای یک write() و read() غیر مسدود کننده استفاده می شود. برای دریافت اعلان عملیات، از فراخوانی دیگری با متد wake() Object EventFlag استفاده کنید. برای تعریف انتزاع EventFlag ، به system/libfmq/include/fmq/EventFlag.h مراجعه کنید.

عملیات کپی صفر

متدهای read ، write ، readBlocking و writeBlocking() یک اشاره گر را به بافر ورودی-خروجی به عنوان آرگومان می گیرند و از فراخوانی های 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() commit کنید. متدهای beginRead و commitRead به همین ترتیب عمل می کنند.
  • متدهای beginRead و Write تعداد پیام‌هایی که باید خوانده و نوشته شوند را به عنوان ورودی می‌گیرند و یک Boolean برمی‌گردانند که نشان می‌دهد خواندن یا نوشتن امکان‌پذیر است. اگر خواندن یا نوشتن امکان پذیر باشد، ساختار 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

به عنوان مثال با استفاده از API های صفر کپی در 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 بین 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 ارسال کنید

در سمت ایجاد:

  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 برای انتقال داده استفاده کنید.