wangzhengquan
2021-01-22 7e1e05df84d57d2d7c3a622d0ece0d4fe7b1fc8c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 基于共享内存的生产者消费者模式的无锁队列的实现
 
## 1 整体设计
 
### 1.1 总的流程如下:
1.  入队操作:队列向内存管理器申请一块内存,内存管理器从内存的自由分配区分配一块内存返回给队列。
2.  出队操作:队列向内存管理器发去释放内存的请求,内存管理器释放该内存块以供下次使用。
 
### 1.2 共享内存的的划分
Queue对象内部记录了队列头和队列尾的位置,通过它们自然可以找到每个队列节点的位置。但是Queue对象自己也在共享内存里面,第一个创建Queue对象的进程自然知道Queue对象的位置,但是必须让其他的进程也知道Queue对象的位置,所以就约定一个规则把Queue对象放在一个大家都知道的位置,这里选择了共享内存的起始位置。内存管理器不会对这块区域进行操作,在此之后的那块内存自由分配区才是内存管理器管理的区域。如下图:
 
 
![](./共享队列综合设计.png)
 
## 2 无锁队列(Free Lock)的实现
无锁队列是用细粒度的原子锁实现的,多进程操作时无阻塞并发性能高。网上有五花八门的相关算法实现。我对比了一下,决定采用下面的这个。这个算法性能好是一方面,它同时也解决了队列为空时并发操作可能出现的问题,以及ABA的问题。
 
![](./code.png)
 
## 3 生产者消费者之间的协作
 
> 以下说明不包括无等待和超时的情况
 
 
生产者消费者之间的相互协作时通过信号实现的,信号量slots代表队列空闲的数量,信号量items代表已插入队列的数量。队列初始化时,slots等于队列的最大长度,items等于0。入队时slots减一,items加1。出队时items减一,slots加1。队列已满时slots的量必然等于0,生产者进入等带状态,有空闲位置的时候会恢复执行。队列为空时items的量必然为0,消费者进入等待状态,有新成员入队的时候会恢复执行。具体协作关系如下图:
![生产者消费者模型](./生产者消费者模型.png)
 
 
## 4 内存管理器
 
要实现对内存的管理,就要记录每个已分配块和空闲块的信息。这些信息就记录在每个块的头部和尾部。这些信息包括块的大小(Block size), 是否已分配(a表示已分配,f表示空闲),其中空闲块比已分配块还多了两个信息(前驱节点指针predecessor, 后继节点指针successor),分别指向前一个和后一个空闲块的指针。如下图:
 
![](./malloc_node.png)
 
Head_listp是一个保留块,里面记录了指向第一个空闲块的指针。每个空闲块又都有前驱节点指针和后继节点指针,这样就形成了一个空闲块双向链表,如图C所示。当用户申请内存的时候,会从Head_listp开始向下遍历,直到找到一个合适大小的空闲块,把该块标记为已分配并移出链表。如果找到的空闲块大于要求的大小太多,空闲块会被分割,一块还留在空闲链表里,一块分配出去使用。初始化的时候有且只有一个大的空闲块,后面对内存的申请都是在这个大块之上不断的切割出来的。这样随着请求的不断产生,内存不断被切割,就会产生太多碎片。所以在释放内存的时候,就应该查看自己的左右相邻的块是否也是空闲的,如果是就应该先合并,再放回空闲块链表。查看自己的左右相邻的块是否是空闲的也很简单,因为该块的左边是左边一个块的Footer,右边是右边一个块的Header,所以很容易找到他们的记录信息。
 
![](./malloc_list.png)