1 关键技术
1.1 基于共享内存的存储和通信
操作系统提供的进程间通信机制有文件、socket、消息队列、管道、共享内存等。其中,共享内存是最快的IPC机制[6]。
共享内存映射到进程空间后,数据可以直接从共享内存进行读写,不需要执行系统调用进行数据的传输。因此,避免了其它进程间通信机制必须的用户态/内核态切换以及用户空间与内核空间的数据拷贝。
由于消息队不需支持跨主机通信,所以可以采用共享内存进行进程间的通信。
1.2 无锁互斥访问
消息队列需要支持多个写者,在多个写者同时进行写操作时,会产生并发操作。处理并发操作,通常的解决方案是使用互斥锁。使用互斥锁简单方便,但有一定的系统开销。在我们的测试环境下,单次锁/解锁操作耗时大概为20纳秒。多个线程互斥操作时,时间开销大大增加。同样的测试条件,当两个线程并发操作时,单次锁/解锁操作的平均时延为320纳秒。
如果并发操作能限制在一个机器字节内,可以使用CPU提供的CAS指令,即’Compare& Swap’原子操作,进行进程间的互斥操作。
相较于使用互斥锁进行多进程间的互斥操作,使用CAS指令的程序“临界区”更小,只有一个字节。当多个写者同时更新“临界区”时,只有一个写者成功,其它写者需要重复操作并检查结果。这样避免了进程锁起时休眠,以及锁操作带来的开销。最大程度的降低响应时延。
无锁程序的关键在于程序设计时,将程序的”临界区”设置为一个机器字节的变量,进而可以使用CAS指令进行原子操作。
1.2.1 消息队列多写者的无锁操作
在消息队列中,数组和数组的头尾head、tail指针是多进程访问的临界区。
只考虑写者,操作临界区为向数组写入数据、tail指针的更新:
critical section begin
write to A[tail]
tail++
critical section end
减少临界区内容,在临界区只对tail指针进行修改:写者竞争获取到tail针后,再写入数据:
critical section begin
s = tail
tail++
critical section end
write to A[s]
现在,临界区只有tail一个变量,使用CAS指令代替互斥锁:
s = tail
do{
next tail = s+1;
ret = CAS(&tail, s, next_tail);
if(s == ret)
break;
s = ret;
}while(1);
write to A[s];
1.2.2 队列读操作
由于只有一个读者,因此不需考虑head指针并发操作的问题。但由于写者先更新tail指针内容,后写入数据,读者在读数据前需要先判断数据是否有效。
消息队列的消息格式如下:
struct {
volatile unsigned int data_len;
char data[0];
};
写者写入数据前,data_len为0,写入数据后,更新data_len内容。此时,读者才开始读数据。读者读取数据后,重置data_len为0;这样,下次写者写入该位置数据完成前,读者不会提前读取。
2 消息队列设计
消息队列不是作为独立的服务,而是设计成库的形式提供使用。应用程序调用消息队列库的接口,编译时将库文件链接到目标文件即可。
按照功能划分,共享内存消息队列划分为消息队列、消息队列创建、消息队列销毁、数据读取、数据写入、消息队列查询等模块,各模块对应的功能如下:
-
消息队列: 提供基础数据结构,缓存写入的数据,供其他模块读取或者查询。
-
消息队列创建: 创建指定键值、大小和容量的共享内存消息队列。
-
消息队列销毁: 销毁指定键值的共享内存消息队列,释放共享内存。
-
数据读取: 读取共享内存消息队列中的数据。
-
数据写入: 向共享内存消息队列中写入数据。
-
消息队列查询: 查询共享内存消息队列的使用情况。
2.1 模块组成
2.1.1 总体组成
共享内存消息队列的组成如下图,其中消息队列由消息队列创建模块创建,供数据读写模块、消息队列查询模块使用,最终被消息队列销毁模块销毁释放。
2.1.2 消息队列组成
消息队列是一个基于共享内存的环形队列,在开辟的一块连续的共享内存中,保存消息队列相关信息,缓存写入的数据。它在内存中的结构定义:
消息队列头包含队列大小、消息记录大小,head、tail位置指针,用于读写同步的条件变量和锁等控制信息。环形队列分别使用首指针head和尾指针tail标记队列的头和尾,读者从head指向的位置读取数据,写者向tail指向的位置写入数据。
2.2 消息队列模块设计
2.2.1 创建和销毁
消息队列应该在应用进程启动前创建,在应用进程退出后再销毁。创建和销毁由独立的程序调用创建和销毁模块进行操作。
创建消息队列过程主要包括:
1. 根据参数确定消息长度、个数,计算队列缓存大小
2. 申请共享内存
3. 初始化消息队列头部控制信息,初始化队列缓存
销毁消息队列过程主要包括:
1. 销毁消息队列头部控制信息
2. 释放共享内存其中控制信息主要包括读者、写者同步需要的条件变量和互斥锁。
2.2.2 数据写入
消息队列支持多个写者同时写操作,多个写者对队列的竞争写由CAS操作完成。写操作的过程如下:
1. 检查环形队列空闲数量是否大于临界值
2. 如果是,执行3;如果否,超时等待,继续执行1
3. 获取当前tail值,new_tail为tail+1
4. 通过CAS指令,使用new_tail更新tail
5. 如果成功,根据new_tail位置写入数据,然后更新该位置消息的data_len为实际数据长度;如果不成功,跳到1继续执行
6. 如果有读者在等待,通知读者
值得注意的是,写者先更新tail指针,后写入数据。这样才能保证多个写者通过CAS进行互斥操作。但是tail指针更新后,不保证数据已经更新完成。读者在读数据时需要根据data_len的值判断数据是否完整。
2.2.3 数据读取
消息队列只有一个读者,逻辑比较简单,读操作过程如下:
1. 检查环形队列是否有数据
2. 如果是,head自加1;如果否,超时等待,执行1
3. 检查环形队列head位置的数据data_len是否为0
4. 如果是,超时等待,若超过500毫秒data_len仍为0,认为写者异常,跳过该位置,执行1;如果否,读取数据
5. 更新该位置data_len为0
6. 如果有写者在等待,通知写者
2.2.4 读写同步
读者和写者使用条件变量
进行同步。条件变量保存在消息队列头部信息中。只有当写者因没有空闲位置进入等待状态时,读者读取数据后才会发送条件变量信号进行通知。同样,只有当读者因没有数据而进入等待状态时,写者写入数据后才发送条件变量信号进行通知。
详细介绍:https://mp.weixin.qq.com/s/RqHsX3NIZ4_BS8O30KWYhQ