無鎖環形緩衝區設計

版權所有 2009 Red Hat Inc.

作者:

Steven Rostedt <srostedt@redhat.com>

許可:

GNU 自由文件許可證,版本 1.2 (在 GPL v2 下雙重許可)

稽核人:

Mathieu Desnoyers, Huang Ying, Hidetoshi Seto, and Frederic Weisbecker.

編寫於: 2.6.31

本文件中使用的術語

尾 (tail)
  • 環形緩衝區中發生新寫入的位置。

頭 (head)
  • 環形緩衝區中發生新讀取的位置。

生產者 (producer)
  • 寫入環形緩衝區的任務 (與寫入者相同)

寫入者 (writer)
  • 與生產者相同

消費者 (consumer)
  • 從緩衝區讀取的任務 (與讀取者相同)

讀取者 (reader)
  • 與消費者相同。

reader_page
  • 環形緩衝區外部的頁面,主要 (大部分情況下) 由讀取者使用。

head_page
  • 指向讀取者將要使用的下一個頁面的指標

tail_page
  • 指向將要寫入的下一個頁面的指標

commit_page
  • 指向具有上次完成的非巢狀寫入的頁面的指標。

cmpxchg
  • 硬體輔助原子事務,執行以下操作

    A = B if previous A == C
    
    R = cmpxchg(A, C, B) is saying that we replace A with B if and only
        if current A is equal to C, and we put the old (current)
        A into R
    
    R gets the previous A regardless if A is updated with B or not.
    

    要檢視更新是否成功,可以使用 R == C 的比較。

通用環形緩衝區

環形緩衝區可以在覆蓋模式或生產者/消費者模式下使用。

生產者/消費者模式是指,如果生產者在消費者釋放任何內容之前填滿了緩衝區,則生產者將停止寫入緩衝區。這將丟失最近的事件。

覆蓋模式是指,如果生產者在消費者釋放任何內容之前填滿了緩衝區,則生產者將覆蓋較舊的資料。這將丟失最舊的事件。

沒有兩個寫入者可以同時寫入 (在同一個 per-cpu 緩衝區上),但是一個寫入者可以中斷另一個寫入者,但是它必須在先前的寫入者可以繼續之前完成寫入。這對演算法非常重要。寫入者的行為就像一個“堆疊”。中斷的工作方式強制執行這種行為

writer1 start
   <preempted> writer2 start
       <preempted> writer3 start
                   writer3 finishes
               writer2 finishes
writer1 finishes

這非常類似於一個寫入者被中斷搶佔,並且中斷也執行寫入。

讀取者可以在任何時候發生。但是沒有兩個讀取者可以同時執行,也不能一個讀取者搶佔/中斷另一個讀取者。讀取者不能搶佔/中斷寫入者,但是它可以與寫入者同時讀取/使用緩衝區,但是讀取者必須在另一個處理器上才能這樣做。讀取者可以在自己的處理器上讀取,並且可以被寫入者搶佔。

寫入者可以搶佔讀取者,但是讀取者不能搶佔寫入者。但是讀取者可以與寫入者同時 (在另一個處理器上) 讀取緩衝區。

環形緩衝區由一個連結串列連結在一起的頁面列表組成。

在初始化時,為不屬於環形緩衝區的讀取者分配一個讀取者頁面。

head_page,tail_page 和 commit_page 都被初始化為指向同一個頁面。

讀取者頁面被初始化為使其 next 指標指向 head 頁面,並且其 previous 指標指向 head 頁面之前的頁面。

讀取者有自己的頁面可以使用。在啟動時,會分配此頁面,但不附加到列表。當讀取者想要從緩衝區讀取時,如果它的頁面為空 (就像在啟動時一樣),它將把它的頁面與 head_page 交換。舊的讀取者頁面將成為環形緩衝區的一部分,並且 head_page 將被刪除。插入的頁面 (舊的 reader_page) 之後的頁面將成為新的 head 頁面。

一旦將新頁面提供給讀取者,只要寫入者離開了該頁面,讀取者就可以對它做任何想做的事情。

讀取者頁面如何交換的示例:注意,這不顯示緩衝區中的 head 頁面,它僅用於演示交換。

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


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

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

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

如果環形緩衝區中的內容小於緩衝區頁面中儲存的內容,則交換的頁面可能是 commit 頁面和 tail 頁面。

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

這種情況對於此演算法仍然有效。當寫入者離開頁面時,它只是進入環形緩衝區,因為讀取者頁面仍然指向環形緩衝區中的下一個位置。

主要指標

讀取者頁面
  • 僅由讀取者使用且不屬於環形緩衝區的頁面 (可能會交換進來)

頭部頁面
  • 環形緩衝區中的下一個頁面,將與讀取者頁面交換。

尾部頁面
  • 下一個寫入將發生的頁面。

提交頁面
  • 上次完成寫入的頁面。

提交頁面僅由寫入者堆疊中最外層的寫入者更新。搶佔另一個寫入者的寫入者不會移動提交頁面。

當資料寫入環形緩衝區時,會在環形緩衝區中保留一個位置並傳遞迴寫入者。當寫入者完成將資料寫入該位置時,它會提交寫入。

在此事務期間,可以在任何時候發生另一個寫入 (或讀取)。如果發生另一個寫入,則必須在繼續上一次寫入之前完成。

寫入保留

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

寫入提交

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

如果在第一次保留後發生寫入

     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 指標指向上次在沒有搶佔另一個寫入的情況下提交的寫入位置。當提交搶佔另一個寫入的寫入時,它僅成為掛起的提交,並且在所有寫入都已提交之前不會成為完全提交。

commit 頁面指向具有上次完全提交的頁面。tail 頁面指向具有上次寫入的頁面 (在提交之前)。

tail 頁面始終等於或晚於 commit 頁面。它可能會提前幾個頁面。如果 tail 頁面趕上 commit 頁面,則無論環形緩衝區的模式如何 (覆蓋和生產者/消費者),都不能再進行寫入。

頁面的順序是

head page
commit page
tail page

可能的場景

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

有一種特殊情況,即頭部頁面在 commit 頁面之後,並且可能在尾部頁面之後。這是當 commit (和 tail) 頁面已與讀取者頁面交換時。這是因為頭部頁面始終是環形緩衝區的一部分,但讀取者頁面不是。只要在環形緩衝區內提交的頁面少於一個完整頁面,並且讀取者交換出一個頁面,它將交換出 commit 頁面。

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

在這種情況下,當 tail 和 commit 移回環形緩衝區時,頭部頁面不會移動。

如果 commit 頁面仍在頁面上,則讀取者無法將頁面交換到環形緩衝區中。如果讀取與上次提交 (真實提交,而不是掛起或保留) 相遇,則沒有更多要讀取的內容。緩衝區被認為是空的,直到另一個完全提交完成。

當 tail 與頭部頁面相遇時,如果緩衝區處於覆蓋模式,則頭部頁面將向前推一個。如果緩衝區處於生產者/消費者模式,則寫入將失敗。

覆蓋模式

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


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


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

請注意,讀取者頁面仍將指向先前的頭部頁面。但是,當發生交換時,它將使用最新的頭部頁面。

使環形緩衝區無鎖:

無鎖演算法背後的主要思想是將 head_page 指標的移動與頁面與讀取者的交換相結合。狀態標誌放置在指向頁面的指標內。為此,每個頁面必須在記憶體中按 4 位元組對齊。這將允許地址的 2 個最低有效位用作標誌,因為對於地址它們將始終為零。要獲取地址,只需遮蔽標誌

MASK = ~3

address & MASK

這兩個位將保留兩個標誌

HEADER
  • 指向的頁面是一個頭部頁面

UPDATE
  • 指向的頁面正在被寫入者更新,並且曾經或即將成為一個頭部頁面。

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

上面的指標“-H->”將設定 HEADER 標誌。也就是說,下一個頁面是要被讀取者交換出的下一個頁面。此指標表示下一個頁面是頭部頁面。

當尾部頁面與頭部指標相遇時,它將使用 cmpxchg 將指標更改為 UPDATE 狀態

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

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

“-U->”表示 UPDATE 狀態下的指標。

對讀取者的任何訪問都需要採取某種鎖來序列化讀取者。但是寫入者永遠不會採取鎖來寫入環形緩衝區。這意味著我們只需要擔心單個讀取者,並且寫入僅以“堆疊”形式搶佔。

當讀取者嘗試將頁面與環形緩衝區交換時,它也將使用 cmpxchg。如果指向頭部頁面的指標中的標誌位沒有設定 HEADER 標誌,則比較將失敗,並且讀取者需要查詢新的頭部頁面並重試。請注意,標誌 UPDATE 和 HEADER 永遠不會同時設定。

讀取者按如下方式交換讀取者頁面

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

讀取者將讀取者頁面的下一個指標設定為 HEADER 到頭部頁面之後的頁面

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

它對指向先前頭部頁面的指標執行 cmpxchg,以使其指向讀取者頁面。請注意,新指標沒有設定 HEADER 標誌。此操作原子地將頭部頁面向前移動

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

在設定新的頭部頁面後,頭部頁面的先前指標會更新為讀取者頁面

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

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

另一個重點:讀取者頁面透過其先前指標指向的頁面 (現在指向新的頭部頁面的頁面) 永遠不會指向讀取者頁面。那是因為讀取者頁面不屬於環形緩衝區。透過 next 指標遍歷環形緩衝區將始終停留在環形緩衝區中。透過 prev 指標遍歷環形緩衝區可能不會。

請注意,確定讀取者頁面的方法很簡單,只需檢查頁面的先前指標即可。如果先前頁面的 next 指標沒有指向原始頁面,則原始頁面是一個讀取者頁面

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

頭部頁面向前移動的方式

當尾部頁面與頭部頁面相遇,並且緩衝區處於覆蓋模式並且發生更多寫入時,必須在寫入者可以移動尾部頁面之前將頭部頁面向前移動。執行此操作的方式是寫入者執行 cmpxchg 以將指向頭部頁面的指標從 HEADER 標誌轉換為設定 UPDATE 標誌。完成此操作後,讀取者將無法從緩衝區交換頭部頁面,也無法移動頭部頁面,直到寫入者完成移動。

這消除了讀取者可能對寫入者造成的任何競爭。讀取者必須旋轉,這就是為什麼讀取者不能搶佔寫入者

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

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

以下頁面將成為新的頭部頁面

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

在設定新的頭部頁面後,我們可以將舊的頭部頁面指標設定回 NORMAL

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

在頭部頁面移動後,尾部頁面現在可以向前移動

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

以上是簡單的更新。現在來看更復雜的場景。

如前所述,如果足夠的寫入搶佔了第一個寫入,則尾部頁面可能會一直繞緩衝區轉,並與 commit 頁面相遇。此時,我們必須開始丟棄寫入 (通常會向用戶發出某種警告)。但是,如果 commit 仍然在讀取者頁面上會發生什麼?commit 頁面不屬於環形緩衝區。尾部頁面必須考慮到這一點

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

如果尾部頁面只是簡單地將頭部頁面向前推,則離開讀取者頁面時的 commit 將不會指向正確的頁面。

對此的解決方案是在推動頭部頁面之前測試 commit 頁面是否在讀取者頁面上。如果是,則可以假定尾部頁面已包裝緩衝區,並且我們必須丟棄新的寫入。

這不是競爭條件,因為 commit 頁面只能由最外層的寫入者 (被搶佔的寫入者) 移動。這意味著當寫入者移動尾部頁面時,commit 不會移動。如果讀取者頁面也用作 commit 頁面,則讀取者無法交換讀取者頁面。讀取者可以簡單地檢查 commit 是否已離開讀取者頁面。一旦 commit 頁面離開讀取者頁面,除非讀取者與也作為 commit 頁面的緩衝區頁面再次進行交換,否則它將永遠不會回到它上面。

巢狀寫入

在向前推動尾部頁面時,如果頭部頁面是下一個頁面,我們必須首先向前推動頭部頁面。如果頭部頁面不是下一個頁面,則只需使用 cmpxchg 更新尾部頁面。

只有寫入者才能移動尾部頁面。必須原子地完成此操作以防止巢狀寫入

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

如果尾部頁面仍指向預期頁面,則以上將更新尾部頁面。如果這失敗了,則巢狀寫入將其向前推動,當前寫入不需要推動它

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

巢狀寫入進入並將尾部頁面向前移動

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

以上將使 cmpxchg 失敗,但是由於尾部頁面已經向前移動,因此寫入者將再次嘗試在新尾部頁面上保留儲存。

但是移動頭部頁面有點複雜

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

寫入將頭部頁面指標轉換為 UPDATE

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

但是,如果巢狀寫入者在此處搶佔,它將看到下一個頁面是一個頭部頁面,但它也是巢狀的。它將檢測到它是巢狀的,並將儲存該資訊。檢測是它看到 UPDATE 標誌而不是 HEADER 或 NORMAL 指標的事實。

巢狀寫入者將設定新的頭部頁面指標

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

但它不會將更新重置為 normal。只有將指標從 HEAD 轉換為 UPDATE 的寫入者才會將其轉換回 NORMAL

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

在巢狀寫入者完成後,最外層的寫入者會將 UPDATE 指標轉換為 NORMAL

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

如果幾個巢狀寫入進入並將尾部頁面向前移動幾個頁面,則它會變得更加複雜

(first writer)

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

寫入將頭部頁面指標轉換為 UPDATE

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

下一個寫入者進入,並看到更新並設定新的頭部頁面

(second writer)

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

巢狀寫入者將尾部頁面向前移動。但不會將舊的更新頁面設定為 NORMAL,因為它不是最外層的寫入者

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

另一個寫入者搶佔並看到尾部頁面之後的頁面是一個頭部頁面。它將其從 HEAD 更改為 UPDATE

(third writer)

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

寫入者將向前移動頭部頁面

(third writer)

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

但是現在第三個寫入者確實將 HEAD 標誌更改為 UPDATE,它將將其轉換為 normal

(third writer)

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

然後它將移動尾部頁面,然後返回到第二個寫入者

(second writer)

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

第二個寫入者將無法移動尾部頁面,因為它已經被移動,因此它將再次嘗試並將其資料新增到新的尾部頁面。它將返回到第一個寫入者

(first writer)

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

第一個寫入者無法原子地知道尾部頁面是否在更新 HEAD 頁面時移動。然後,它將更新頭部頁面,以它認為的新頭部頁面

(first writer)

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

由於 cmpxchg 返回指標的舊值,因此第一個寫入者將看到它成功地將指標從 NORMAL 更新為 HEAD。但是正如我們所看到的,這還不夠好。它還必須檢查尾部頁面是否在它過去的位置或在下一個頁面上

(first writer)

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

如果 tail 頁面 != A 並且 tail 頁面 != B,則它必須將指標重置回 NORMAL。它只需要擔心巢狀寫入者這一事實意味著它只需要在設定 HEAD 頁面後檢查此項

(first writer)

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

現在,寫入者可以更新頭部頁面。這也是頭部頁面必須保持 UPDATE 狀態並且只能由最外層的寫入者重置的原因。這可以防止讀取者看到不正確的頭部頁面

(first writer)

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