目录
无锁环形缓冲区设计原理
阅读linux源码首先从理解原理开始,linux文档已经整理的很好了,直接查看源码目录里的Documentation/trace/ring-buffer-design.txt,以下提供本人的翻译和解释。
本文中使用的术语
tail - 在环形缓冲区中发生新写入的位置。
head - 在环形缓冲区中发生新读取的位置。
producer(生产者) - 写入环形缓冲区的任务(与writer相同)。
writer(写者) - 与producer 相同。
consumer(消费者) - 从缓冲区读取的任务(与writer相同)。
reader (读者)- 与consumer相同。
reader_page - 一个(大部分)由读者提供的在环形缓冲区外使用的页面。
head_page - 指向读者下一步将使用的页面的指针。
tail_page - 指向将被写入的下一页的页面的指针。
commit_page - 指向最后一次完成非嵌套写入的页面的指针。
cmpxchg - 硬件支持的原子操作,执行以下操作:
(如果 A == C ,A = B)
R = cmpxchg(A,C,B)//是指当且仅当现在的A等于C,将B写入A,然后把老的(现在)的A写入R。R获取的是之前的A的值,而不管A是否更新为B。要查看更新是否成功,可以使用R==C比较。(注:也就是说不管A的值是什么,R的值是原来A的值,判断R==C为true,说明A成功赋上B的值了,R==C为false说明失败了)
通用环形缓冲区概述
-----------------------
环形缓冲区可以在覆盖模式下使用,也可以在生产者/消费者模式下使用。
生产者/消费者模式,是指生产者填满缓冲区的时候,消费者消费任何东西之前,生产者就会停止写缓冲区。这将丢失最近的事件。
覆盖模式,是指生产者填满缓冲区的时候,消费者消费任何东西之前,生产者将覆盖旧的数据。这将丢失最早的事件。
两个写入者(writers)不能同时写入(在相同的per-cpu缓冲区上),但是一个写入者可以打断另一个写入者,但他必须在前一个写入者可以继续写入之前完成写入。这对算法来说非常重要。写入者的行为就像一个栈。中断的方式会强制执行这种行为。
writer1 start
<preempted> writer2 start
<preempted> writer3 start
writer3 finishes
writer2 finishes
writer1 finishes
这非常像是一个写入者被一个中断抢占了,并且这个中断也在进行写操作。
读者可以随时读。但是并没有两个读者同时在运行,同时一个读者也不能抢占/中断另一个读者。读者无法抢占/中断写入程序,它可以从缓冲区读出/消费的同时写者写入,但读者必须在另一个处理器上执行此操作。
写者可以抢占读者,但读者不能抢占写者。读者可以读取缓冲区,同时在另一个处理器上作为写者。
环形缓冲区由一个链表保存的页面组成。
初始化时,为读者分配的页面不是环形缓冲区的一部分。
head_page, tail_page 和commit_page页都初始化指向同一个页面。
读者页面被初始化时,next指针指向head page,同时它的previous指针指向head page的前一页。
读者有自己的页面可供使用。在启动时,此页面就是已分配好的,只是没有加到列表中。当读者想要要从缓冲区读取,如果其页面为空(如启动时),它将与head page交换页面。老的读者页面将成为环形缓冲区的一部分,head_page将被移出。插入的页面(老的reader_page)将成为新的head page。
一旦新页面交给读者,只要一个写者离开了那一个页面读者可以做任何处理。
一个如何交换读者页面的示例:注意,这并不是真实的在缓冲区中的head page,仅仅是演示:
+------+
|reader| RING BUFFER
|page |
+------+
+---+ +---+ +---+
| |-->| |-->| |
| |<--| |<--| |
+---+ +---+ +---+
^ | ^ |
| +-------------+ |
+-----------------+
+------+
|reader| RING BUFFER
|page |-------------------+
+------+ v
| +---+ +---+ +---+
| | |-->| |-->| |
| | |<--| |<--| |<-+
| +---+ +---+ +---+ |
| ^ | ^ | |
| | +-------------+ | |
| +-----------------+ |
+------------------------------------+
+------+
|reader| RING BUFFER
|page |-------------------+
+------+ <---------------+ v
| ^ +---+ +---+ +---+
| | | |-->| |-->| |
| | | | | |<--| |<-+
| | +---+ +---+ +---+ |
| | | ^ | |
| | +-------------+ | |
| +-----------------------------+ |
+------------------------------------+
+------+
|buffer| RING BUFFER
|page |-------------------+
+------+ <---------------+ v
| ^ +---+ +---+ +---+
| | | | | |-->| |
| | New | | | |<--| |<-+
| | Reader +---+ +---+ +---+ |
| | page ----^ | |
| | | |
| +-----------------------------+ |
+------------------------------------+
交换的页面可能是commit page和tail page,如果在环缓冲区中保存的内容小于在buffer page中保存的内容。
reader page commit page tail page
| | |
v | |
+---+ | |
| |<----------+ |
| |<------------------------+
| |------+
+---+ |
|
v
+---+ +---+ +---+ +---+
<---| |--->| |--->| |--->| |--->
--->| |<---| |<---| |<---| |<---
+---+ +---+ +---+ +---+
这种情况对该算法仍然有效。
当写入者离开这个页面时,它(页面)只是进入环形缓冲区,因为读者页面仍然指向环形缓冲区的下一个位置。
主要指针
reader page - 仅由读者使用的页面,不是 环形缓冲区的组成部分(可被换入)。
head page - 环形缓冲区中要和读者交换的下一个页面。
tail page - 下一次写入的页面。
commit page - 上次完成写入的页面。
commit page