linux5.2内核的ringbuffer(无锁环形缓冲区)原理实现与源码分析(上)

目录

        无锁环形缓冲区设计原理

        本文中使用的术语

        通用环形缓冲区概述

       主要指针

       使环形缓冲器lockless(无锁)

       嵌套写入


        无锁环形缓冲区设计原理

        阅读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仅由writer stack的最上面那个写者更新。一个写者抢占其他写者不会移动commit page。

        当数据写入环形缓冲区时,会在环形缓冲区中保留(reserved)一个位置,并传回给写者。当写者将数据写入该位置完成后,它将提交这次写入。

        在此次操作期间的任何时候都可能发生另一次写入(或读取)。如果发生了另一次写操作,这个新操作必须先完成,然后继续前一次写操作。

   Write reserve:

       Buffer page
      +---------+
      |written  |
      +---------+  <--- given back to writer (current commit)
      |reserved |
      +---------+ <--- tail pointer
      | empty   |
      +---------+

   Write commit:

       Buffer page
      +---------+
      |written  |
      +---------+
      |written  |
      +---------+  <--- next position for write (current commit)
      | empty   |
      +---------+

   

   If a write happens after the first reserve:
    Buffer page
      +---------+
      |written  |
      +---------+  <-- current commit
      |reserved |
      +---------+  <--- given back to second writer
      |reserved |
      +---------+ <--- tail pointer

   After second writer commits:


       Buffer page
      +---------+
      |written  |
      +---------+  <--(last full commit)
      |reserved |
      +---------+
      |pending  |
      |commit   |
      +---------+ <--- tail pointer

  When the first writer commits:

       Buffer page
      +---------+
      |written  |
      +---------+
      |written  |
      +---------+
      |written  |
      +---------+  <--(last full commit and tail pointer)

        在不抢占另一次写入的情况下,commit指针指向最后一个被提交的写入的位置。当一个写者抢占了另一个已经提交的写者,则它只会成为pending commit(挂起的提交)而不是一个full commit(完整的提交)直到所有的写入都被提交完成。

        commit page指向最后一个full commit的页面。tail page 指向最后的写入页面(在提交前的)。

        tail page始终等于或在commit page之后。它可能就在几个页面前。如果tail page赶上commit page页,则不会再进行写入(无论哪种模式的环形缓冲区:覆盖和生产/消费者)。

        页的顺序为:

         head page
         commit page
         tail page

        可能的情况:

                             tail page
  head page         commit page  |
      |                 |        |
      v                 v        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        有一种特殊情况,就是head page在commit page之后,并且可能在tail page之后。那种情况是当commit (和tail) page已经被reader page交换的时候。这是因为head page总是是环形缓冲区的一部分,但reader page不是。每当环缓冲区中已提交的页面少于一个满页面,并且一个读者换出一个页面时,它将会换出commit page。

          reader page    commit page   tail page
              |              |             |
              v              |             |
             +---+           |             |
             |   |<----------+             |
             |   |<------------------------+
             |   |------+
             +---+      |
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+
                        ^
                        |
                    head page

        在这种情况下,当tail和commit移回到环形缓冲区时,head page不会移动。
        如果commit page仍然指在这个页面上,则读者无法将页面交换到环形缓冲区中。如果读取满足last commit(实际提交没有pending 或者 reserved),那么就没有什么可读的了。在另一次full commit完成之前,缓冲区被视为空的。
        当tail与head页相遇时,如果缓冲区处于覆盖模式,head page将向前推一页。如果缓冲区位于生产者/消费者中模式下,写入将失败。
        覆盖模式:

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+
                        ^
                        |
                    head page


            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+
                                 ^
                                 |
                             head page


                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+
                                 ^
                                 |
                             head page

        注意,reader page仍将指向上一个head page。但当交换发生时,它将使用最新的head page。


       使环形缓冲器lockless(无锁)


--------------------------------
        无锁算法背后的主要思想是将移动head_page指针与读者交换页面结合起来。状态标志放置在指向页面的指针内。为此,每个页面必须在内存中保持4个字节对齐。这将允许地址的2个最低有效位要用作标志,因为它们在这个地址里始终为零。要得到这个地址,只需mask掩码这个标志位。

        MASK=~3
        address & MASK
        这两个位将保存两个标志:
        HEADER - 被指向的页面是head page。
        UPDATE - 被指向的页面是一个被写者更新过的,并且现在是或即将变成一个head page。

        reader page
              |
              v
             +---+
             |   |------+
             +---+      |
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-H->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        上面的指针“-H->”将设置成HEADER的flag。标志的下一个页面就是读者要换出的下一个页。这个指针表示下一个页面是head page。
        当tail page遇到head指针时,它将使用cmpxchg更改指向UPDATE状态的指针:

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-H->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        “-U->”表示处于UPDATE 状态的指针。
        对读者的任何访问都需要使用某种锁来序列化读者。但读者永远不会拿着锁去写环形缓冲器。这意味着我们只需要担心一个读者,并写入仅抢占以“堆栈”形式。
        当读者尝试用环形缓冲区交换页面时,它也将会使用cmpxchg。如果指针中的flag标志位指向
没有设置HEADER flag的head page,比较将失败并且读者需要寻找新的head page,然后重试。
注意,UPDATE和HEADER的flag标志不要同时设置。
        读者交换读者页面,如下所示:

  +------+
  |reader|          RING BUFFER
  |page  |
  +------+
                  +---+    +---+    +---+
                  |   |--->|   |--->|   |
                  |   |<---|   |<---|   |
                  +---+    +---+    +---+
                   ^ |               ^ |
                   | +---------------+ |
                   +-----H-------------+

        读者将读者页面next指针作为HEADER设置到head page后面的page。

  +------+
  |reader|          RING BUFFER
  |page  |-------H-----------+
  +------+                   v
    |             +---+    +---+    +---+
    |             |   |--->|   |--->|   |
    |             |   |<---|   |<---|   |<-+
    |             +---+    +---+    +---+  |
    |              ^ |               ^ |   |
    |              | +---------------+ |   |
    |              +-----H-------------+   |
    +--------------------------------------+

        它使用指向上一个head page的指针执行cmpxchg以使其指向读者页面。请注意,新指针没有HEADER flag的设置。此操作会自动向前移动head page。

  +------+
  |reader|          RING BUFFER
  |page  |-------H-----------+
  +------+                   v
    |  ^          +---+   +---+   +---+
    |  |          |   |-->|   |-->|   |
    |  |          |   |<--|   |<--|   |<-+
    |  |          +---+   +---+   +---+  |
    |  |             |             ^ |   |
    |  |             +-------------+ |   |
    |  +-----------------------------+   |
    +------------------------------------+

        设置新的head page后,head page的previous 指针将被更新到读者页面。

  +------+
  |reader|          RING BUFFER
  |page  |-------H-----------+
  +------+ <---------------+ v
    |  ^          +---+   +---+   +---+
    |  |          |   |-->|   |-->|   |
    |  |          |   |   |   |<--|   |<-+
    |  |          +---+   +---+   +---+  |
    |  |             |             ^ |   |
    |  |             +-------------+ |   |
    |  +-----------------------------+   |
    +------------------------------------+

  +------+
  |buffer|          RING BUFFER
  |page  |-------H-----------+  <--- New head page
  +------+ <---------------+ v
    |  ^          +---+   +---+   +---+
    |  |          |   |   |   |-->|   |
    |  |  New     |   |   |   |<--|   |<-+
    |  | Reader   +---+   +---+   +---+  |
    |  |  page ----^                 |   |
    |  |                             |   |
    |  +-----------------------------+   |
    +------------------------------------+

        另一个要点:读者页面通过其previous 指针(现在指向新head page的指针)指向的页面永远不要指向读者页面。这是因为读者页面不是环缓冲区的一部分。通过next指针遍历环形缓冲区将始终停留在环形缓冲区中。通过previous 指针遍历环形缓冲区则可能不是。
        注意,确定读者页面的方法只需检查前页面的previous 指针。如果上一页的next指针没有指向原始页面,那么原始页面就是读者页面:

             +--------+
             | reader |  next   +----+
             |  page  |-------->|    |<====== (buffer page)
             +--------+         +----+
                 |                | ^
                 |                v | next
            prev |              +----+
                 +------------->|    |
                                +----+


        head page向前移动的方式:
        当tail page与head page相遇且缓冲区处于覆盖模式时,并且如果发生更多的写入,那么在写者可能移动tail page之前head page必须被移动。方法是写者执行cmpxchg转换head page指针,从HEADER flag到UPDATE flag的设置。一旦设置完成,读者将无法从缓冲区交换head page,也无法移动head page,直到写者完成移动。
        这就消除了读者对写者的任何竞争。读者必须自旋,这就是为什么读者不能先于写者。
 

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-H->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

         下一页将成为新的head page。
 

           tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        在设置了新的head page之后,我们可以设置旧的head page指针回到NORMAL。
 

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        head page移动后,tail page现在可以向前移动。
 

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        以上是琐碎的更新。现在来看更复杂的场景。
        如前所述,如果有足够的写入抢占第一次写入,则tail page可能会环绕缓冲区并与commit
page相遇。此时,我们必须开始删除写操作(通常是向用户发出某种类型的警告)。但是,如果提交仍在reader page中,会发生什么?commit page不是环形缓冲区的一部分。tail page必须对此作出解释。

          reader page    commit page
              |              |
              v              |
             +---+           |
             |   |<----------+
             |   |
             |   |------+
             +---+      |
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-H->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+
               ^
               |
           tail page

        如果tail page只是将head page向前push(推),那么这个提交在离开读者页面时不会指向正确的页面。
        解决方案是测试在push(推) head page之前commit page是否在读者页面上。如果是,则可以假设tail page包裹了缓冲区,我们必须删除新的写操作。
        这不是竞态条件(race condition),因为commit page只能由最外层的写者(被抢占的写者)移动。
        这意味着当写者正在移动tail page时,这个提交将不会移动。如果读者页面正在被当作commit page使用,那么读者无法交换该读者页面。读者只需检查提交已从读者页面删除。一旦commit page离开读者页面,它将不再返回,除非读者再次与缓冲区页换页,那也是commit page。

       嵌套写入


-------------
        在推进tail page时,我们必须先向前推进head page(如果head page是下一页)。如果head page不是下一页,tail page只是用cmpxchg更新。
        只有写者才能移动tail page。这必须以原子方式进行,以保护嵌套写入。


        temp_page = tail_page
        next_page = temp_page->next
        cmpxchg(tail_page, temp_page, next_page)


        如果仍然指向预期的页面,则上面的代码将更新tail page。如果失败,嵌套写入将其向前推,即当前写入不需要push。

           temp page
               |
               v
            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        嵌套写入将进入并向前移动tail page:

                    tail page (moved by nested writer)
            temp page   |
               |        |
               v        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        上述操作将使cmpxchg失败,但由于tail page已经向前移动,写者将再次尝试保留存储
在新的tail page上。
        但是head page的移动有点复杂。

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-H->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        写入操作将head page指针转换为UPDATE。

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        但是如果嵌套写者抢占了这里,它将看到下一个页面是head page,但也是嵌套的。它将检测到它是嵌套的,并且将保存该信息。它的检测事实上看到的指针是UPDATE flag,而不是HEADER或NORMAL。
        嵌套写者将设置新的head page指针。

           tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        但它不会将更新重置回正常状态。只有写者将指针从HEAD转换为UPDATE才会把它转换回NORMAL。

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

         嵌套写者完成后,最外层的写者将转换指针的UPDATE为NORMAL。

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        如果有多个嵌套写入进入并移动tail page到几个页面前,则可能会更加复杂:

(first writer)

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-H->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        写入操作将头页指针转换为UPDATE。 

            tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        下一个写者进入,查看更新并设置新的head page。

(second writer)

           tail page
               |
               v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        嵌套写者将tail page向前移动。但这并不能改变旧的update page
为NORMAL,因为它不是最外层的写者。

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        另一位写者抢占,并且在tail page之后看到的是head page。它将它从HEAD更改为UPDATE。

(third writer)

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-U->|   |--->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        写者将向前移动head page:

(third writer)

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-U->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        但是现在第三个写者确实更改了HEAD flag变成UPDATE,它将其转换为正常:

(third writer)

                    tail page
                        |
                        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        然后它将移动tail page,并返回给第二个写者。

(second writer)

                             tail page
                                 |
                                 v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        第二个写者将无法移动tail page,因为它已被移动,因此它将重试然后将其数据添加到新的tail page.。
        它将返回给第一个写者。

(first writer)

                             tail page
                                 |
                                 v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        当第一个写者更新HEAD page时无法自动知道tail page是否移动。然后,它会将head page更新为它认为的新的head page。

(first writer)

                             tail page
                                 |
                                 v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

         由于cmpxchg返回指针的旧值,因此第一个写者将看到它成功地将指针从NORMAL更新为HEAD。
        但正如我们所看到的,这还不够好。它还必须检查是否tail page位于它原来所在的位置或在下一页上:

(first writer)

               A        B    tail page
               |        |        |
               v        v        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |-H->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        如果tail page != A 并且tail page != B,那么 它必须重置指针恢复到NORMAL。事实上,它只需要担心嵌套写者,意味着它只需要在设置HEAD page之后检查这个。

(first writer)

               A        B    tail page
               |        |        |
               v        v        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |-U->|   |--->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+

        现在,写者可以更新head page了。这也是为什么head page必须保持UPDATE,并且仅由最外层的写者重置的原因。这防止了读者看到错误的head page。

(first writer)

               A        B    tail page
               |        |        |
               v        v        v
    +---+    +---+    +---+    +---+
<---|   |--->|   |--->|   |--->|   |-H->
--->|   |<---|   |<---|   |<---|   |<---
    +---+    +---+    +---+    +---+


 

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核中的无(lock-free)技术主要用于实现高效的并发数据结构,以提高系统的性能和吞吐量。其中,无环形缓冲区(lock-free ring buffer)是一种常用的数据结构,它可以高效地实现在多个线程之间传递数据的功能。 无环形缓冲区实现原理如下: 1. 环形缓冲区的数据结构:无环形缓冲区由一个固定大小的环形数组和两个指针构成,一个是读指针,一个是写指针。读指针指向下一个将要读取的元素,写指针指向下一个将要写入的元素。 2. 原子操作:无环形缓冲区实现依赖于原子操作(atomic operations),这些操作是在单个CPU指令中执行的,不会被其他线程中断。在Linux内核中,原子操作是通过宏定义实现的,如“atomic_add()”、“atomic_sub()”等。 3. 写入数据:当一个线程想要写入数据时,它首先需要检查缓冲区是否已满。如果缓冲区已满,则写入操作失败。如果缓冲区未满,则该线程会使用原子操作将数据写入缓冲区,并更新写指针。 4. 读取数据:当一个线程想要读取数据时,它首先需要检查缓冲区是否为空。如果缓冲区为空,则读取操作失败。如果缓冲区不为空,则该线程会使用原子操作将数据从缓冲区中读取,并更新读指针。 5. 线程同步:无环形缓冲区实现不依赖于任何机制,因此可以避免竞争和死等问题。不过,在多个线程并发读写的情况下,需要使用一些同步机制来保证线程安全,如使用原子操作或者memory barrier等技术。 总的来说,无环形缓冲区是一种高效的并发数据结构,能够在多个线程之间高效地传递数据,提高系统的性能和吞吐量。在Linux内核中,无环形缓冲区实现依赖于原子操作和线程同步技术,可以避免竞争和死等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值