قائمة انتظار الرسائل السريعة (FMQ)

تستخدم البنية التحتية لاستدعاء الإجراء البعيد (RPC) لـ HIDL آليات Binder ، مما يعني أن الاستدعاءات تتضمن حملًا زائدًا ، وتتطلب عمليات kernel ، وقد تؤدي إلى إجراء الجدولة. ومع ذلك ، في الحالات التي يجب فيها نقل البيانات بين العمليات بأقل عبء وبدون تدخل kernel ، يتم استخدام نظام Fast Message Queue (FMQ).

يقوم FMQ بإنشاء قوائم انتظار الرسائل بالخصائص المطلوبة. يمكن إرسال كائن MQDescriptorSync أو MQDescriptorUnsync عبر استدعاء HIDL RPC واستخدامه بواسطة عملية الاستلام للوصول إلى قائمة انتظار الرسائل.

يتم دعم قوائم انتظار الرسائل السريعة فقط في C ++ وعلى الأجهزة التي تعمل بنظام Android 8.0 والإصدارات الأحدث.

أنواع MessageQueue

يدعم 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 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 هما bitmasks 32 بت.

readBlocking() ينتظر بتات writeNotification ؛ إذا كانت هذه المعلمة تساوي 0 ، يفشل الاستدعاء دائمًا. إذا كانت قيمة readNotification هي 0 ، فلن تفشل المكالمة ، لكن القراءة الناجحة لن تعين أي بتات إعلام. في قائمة انتظار متزامنة ، قد يعني هذا أن استدعاء writeBlocking() المقابل لن يتم تنشيطه أبدًا ما لم يتم تعيين البت في مكان آخر. في قائمة انتظار غير متزامنة ، لن تنتظر writeBlocking() (لا يزال يتعين استخدامها لتعيين بت إعلام الكتابة) ، ومن المناسب للقراءات عدم تعيين أي بتات إعلام. وبالمثل ، writeblocking() إذا كانت readNotification 0 ، وتعين الكتابة الناجحة بتات writeNotification المحددة.

للانتظار في قوائم انتظار متعددة في وقت واحد ، استخدم طريقة wait() الخاصة بكائن EventFlag للانتظار على قناع بت للإعلامات. تقوم طريقة wait() بإرجاع كلمة حالة مع وحدات البت التي تسببت في إعداد التنبيه. تُستخدم هذه المعلومات بعد ذلك للتحقق من أن قائمة الانتظار المقابلة بها مساحة كافية أو بيانات لعملية الكتابة / القراءة المطلوبة وإجراء write() / read() . للحصول على إشعار بعد العملية ، استخدم استدعاء آخر EventFlag 's wake() . لتعريف تجريد EventFlag ، ارجع إلى system/libfmq/include/fmq/EventFlag.h .

عمليات النسخ الصفري

تأخذ واجهات برمجة التطبيقات read / write / readBlocking writeBlocking() مؤشرًا إلى المخزن المؤقت للإدخال / الإخراج كوسيطة وتستخدم مكالمات memcpy() داخليًا لنسخ البيانات بين المخزن المؤقت الدائري FMQ نفسه. لتحسين الأداء ، يشتمل Android 8.0 والإصدارات الأحدث على مجموعة من واجهات برمجة التطبيقات التي توفر وصولاً مباشرًا للمؤشر إلى المخزن المؤقت الحلقي ، مما يلغي الحاجة إلى استخدام مكالمات memcpy .

استخدم واجهات برمجة التطبيقات العامة التالية لعمليات 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 s داخل كائن 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 هو بين 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 لنقل البيانات.