快速消息队列 (FMQ)
HIDL 的远程过程调用 (RPC) 基础架构使用 Binder 机制,这意味着调用涉及开销、需要内核操作,并且可以触发调度程序操作。
不过,对于必须在开销较小且无内核参与的进程之间传输数据的情况,则使用快速消息队列 (FMQ) 系统。
FMQ 会创建具有所需属性的消息队列。MQDescriptorSync 或 MQDescriptorUnsync 对象可通过 HIDL RPC 调用发送,并可供接收进程用于访问消息队列。
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
(在本示例中)可以是任意不涉及嵌套式缓冲区(无string
或vec
类型)、句柄或接口的 HIDL 定义的类型。kNumElementsInQueue
表示队列的大小(以条目数表示);它用于确定将为队列分配的共享内存缓冲区的大小。
创建第二个 MessageQueue 对象
使用从消息队列的第一侧获取的 MQDescriptor
对象创建消息队列的第二侧。通过 HIDL RPC 调用将 MQDescriptor
对象发送到将容纳消息队列末端的进程。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 对象,并允许指定要使用的通知位掩码。在这种情况下,必须为每个读取和写入调用提供事件标记和位掩码。
对于长格式,可在每个 readBlocking() 和 writeBlocking() 调用中显式提供 EventFlag。
可以将其中一个队列初始化为包含一个内部事件标记,如果是这样,则必须使用 getEventFlagWord() 从相应队列的 MessageQueue 对象中提取该标记,以用于在每个进程中创建与其他 FMQ 一起使用的 EventFlag 对象。或者,可以将 EventFlag 对象初始化为具有任何合适的共享内存。
一般来说,每个队列都应只使用以下三项之一:非阻塞、短格式阻塞,或长格式阻塞。混合使用也不算是错误;但要获得理想结果,则需要谨慎地进行编程。
使用 MessageQueue
MessageQueue 对象的公共 API 是: