4.環形緩沖區庫
環形緩沖區支持隊列管理。rte_ring并不是具有無限大小的鏈表,它具有如下屬性:
- 先進先出(FIFO)
- 最大大小固定,指針存儲在表中
- 無鎖實現
- 多消費者或單消費者出隊操作
- 多生產者或單生產者入隊操作
- 批量出隊 - 如果成功,將指定數量的元素出隊,否則什么也不做
- 批量入隊 - 如果成功,將指定數量的元素入隊,否則什么也不做
- 突發出隊 - 如果指定的數目出隊失敗,則將最大可用數目對象出隊
- 突發入隊 - 如果指定的數目入隊失敗,則將最大可入隊數目對象入隊
相比于鏈表,這個數據結構的優點如下:
- 更快,只需要一個sizeof(void *)的Compare-And-Swap指令,而不是多個雙重比較和交換指令。
- 更像是一個完全無鎖隊列。
- 適應批量入隊/出隊操作。因為指針是存儲在表中的,多個對象的出隊將不會產生與鏈表隊列中一樣多的cache miss。此外,批量出隊成本并不比單個對象出隊高。
缺點:
- 大小固定。
- 大量ring相比于鏈表,消耗更多的內存,空ring至少包含n個指針。
數據結構中存儲的生產者和消費者頭部和尾部指針顯示了一個簡化版本的ring。
4.1.FreeBSD*中環形緩沖區實現參考
FreeBSD 8.0中添加了如下代碼,并應用到了某些網絡設備驅動程序中(至少Interl驅動中應用了):
4.2.Linux*中的無鎖環形緩沖區
參考鏈接Linux Lockless Ring Buffer Design。
4.3.附加特性
4.3.1.名字
每個ring都有唯一的名字。 用戶不可能創建兩個具有相同名稱的ring(如果嘗試調用rte_ring_create()這樣做的話,將返回NULL)。
4.4.用例
Ring庫的使用情況包括:
- DPDK應用程序之間的交互
- 用于內存池申請
4.5.環形緩沖區解析
本節介紹ring buffer的運行方式。Ring結構有兩組頭尾指針組成,一組被生產者調用,一組被消費者調用。以下將簡單稱為prod_head、prod_tail、cons_head及cons_tail。
每個圖代表了ring的簡化狀態,是一個循環緩沖器。本地變量的內容在圖上方表示,Ring結構的內容在圖下方表示。
4.5.1.單生產者入隊
本節介紹了一個生產者向隊列添加對象的情況。在本例中,只有生產者頭和尾指針(prod_head and prod_tail)被修改,只有一個生產者。初始狀態是將prod_head和prod_tail指向相同的位置。
4.5.1.1.入隊第一步
首先, ring->prod_head和ring->cons_tail復制到本地變量中。prod_next本地變量指向下一個元素,或者,如果是批量入隊的話,指向下幾個元素。如果ring中沒有足夠的空間存儲元素的話(通過檢查cons_tail來確定),則返回錯誤。
4.5.1.2.入隊第二步
第二步是在環結構中修改 ring->prod_head,以指向與prod_next相同的位置。指向待添加對象的指針被復制到ring中。
4.5.1.3.入隊最后一步
一旦將對象添加到ring中,ring結構中的 ring->prod_tail 將被修改,指向與 ring->prod_head 相同的位置。入隊操作完成。
4.5.2.單消費者出隊
本節介紹一個消費者從ring中取出對象的情況。在本例中,只有消費者頭尾指針(cons_head and cons_tail)被修改,只有一個消費者。初始狀態是將cons_head和cons_tail指向相同位置。
4.5.2.1.出隊第一步
首先,將 ring->cons_head 和 ring->prod_tail*復制到局部變量中。 *cons_next 本地變量指向表的下一個元素,或者在批量出隊的情況下指向下幾個元素。如果ring中沒有足夠的對象用于出隊(通過檢查prod_tail),將返回錯誤。
4.5.2.2.出隊第二步
第二步是修改ring結構中 ring->cons_head,以指向cons_next相同的位置。指向出隊對象(obj1) 的指針被復制到用戶指定的指針中。
4.5.2.3. 出隊最后一步
最后,ring中的ring->cons_tail被修改為指向ring->cons_head相同的位置。 出隊操作完成。
4.5.3.多生產者入隊
本節說明兩個生產者同時向ring中添加對象的情況。在本例中,僅修改生產者頭尾指針(prod_head和prod_tail)。初始狀態是將prod_head 和 prod_tail 指向相同的位置。
4.5.3.1. 多生產者入隊第一步
在生產者的兩個core上, ring->prod_head 及 ring->cons_tail 都被復制到局部變量。 局部變量prod_next指向下一個元素,或者在批量入隊的情況下指向下幾個元素。
如果ring中沒有足夠的空間用于入隊(通過檢查cons_tail),將返回錯誤。
4.5.3.2.多生產者入隊第二步
第二步是修改ring結構中 ring->prod_head,來指向prod_next相同的位置。 此操作使用比較和交換(CAS)指令,該指令以原子操作的方式執行以下操作:
如果ring->prod_head 與本地變量prod_head不同,則CAS操作失敗,代碼將在第一步重新啟動。
否則,ring->prod_head設置為本地變量prod_next,CAS操作成功并繼續下一步處理。
在圖中,core1執行成功,core2重新啟動步驟1。
4.5.3.3.多生產者入隊第三步
core 2的CAS操作成功重試。
core 1更新一個對象(obj4)到ring上。Core 2更新一個對象(obj5)到ring上
4.5.3.4.多生產者入隊第四步
每個core現在都想更新 ring->prod_tail。 只有ring->prod_tail等于prod_head本地變量,core才能更新它。 當前只有core 1滿足,操作在core 1上完成。
4.5.3.5.多生產者入隊最后一步
一旦ring->prod_tail被core 1更新完,core 2也滿足條件,允許更新。core 2上也完成了操作。
4.5.4.32-bit模索引值
在前面的圖中,prod_head,prod_tail,cons_head和cons_tail索引由箭頭表示。但是,在實際實現中,這些值不會假定在0~(size(ring)-1)之間。 索引值在0~(232-1)之間,當我們訪問ring本身時,我們掩碼取值。32bit模數也意味著如果溢出32bit的范圍,對索引的操作將自動執行232模。
以下是兩個例子,用于幫助解釋索引值如何在ring中使用。
注意:為了簡化說明,使用模16bit操作,而不是32bit。另外,四個索引被定義為16bit無符號整數,與實際情況下的32bit無符號數相反。
這個ring包含11000對象。
這個ring包含12536個對象。
注意:為了便于理解,我們在上面的例子中使用模65536操作。 在實際執行情況下,這種低效操作是多余的,但是,當溢出時會自動執行。
代碼始終保證生產者和消費者之間的距離在0~(size(ring)-1)之間。基于這個屬性,我們可以對兩個索引值做減法,而不用考慮溢出問題。
任何情況下,ring中的對象和空閑對象都在0~(size(ring)-1)之間,即便第一個減法操作已經溢出:
uint32_t entries = (prod_tail – cons_head);
uint32_t free_entries = (mask + cons_tail – prod_head);