參考資料:
《深入淺出DPDK》
DPDK官網:http://doc.dpdk.org/guides/prog_guide/
前言
前面章節我們已經對DPDK多核處理器做了分析,遵循資源局部化原則,解藕數據的跨核共享,使得性能可以有很好的水平擴展。但是,在實際情況下,CPU之間不同核的數據通信,數劇同步,臨界區的保護等都是要面臨的問題,這節主要准對這個問題來的
一. DPDK原子操作實現和應用
1)我們先介紹一下原子操作以及為什么DPDK中會用到原子操作
所謂原子操作,就是“不可中斷的一個或一系列操作”。在單核心處理器系統中,能夠在一條機器指令中完成的操作都可以認為是原子操作,因為中斷只能發生於指令之間。這也是某些CPU指令系統中引入了test_and_set、test_and_clear等指令用於臨界資源互斥的原因。
在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由於系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。
2)原子操作API
DPDK代碼中提供了16,32和64位原子操作的API,以ret_atomic64_add() API源代碼為例
二. DPDK無鎖環形緩沖區
1) rte_ring 的數據結構定義
DPDK中的rte_ring的數據結構定義
1 /** 2 * An RTE ring structure. 3 * 4 * The producer and the consumer have a head and a tail index. The particularity 5 * of these index is that they are not between 0 and size(ring). These indexes 6 * are between 0 and 2^32, and we mask their value when we access the ring[] 7 * field. Thanks to this assumption, we can do subtractions between 2 index 8 * values in a modulo-32bit base: that's why the overflow of the indexes is not 9 * a problem. 10 */ 11 struct rte_ring { 12 /* 13 * Note: this field kept the RTE_MEMZONE_NAMESIZE size due to ABI 14 * compatibility requirements, it could be changed to RTE_RING_NAMESIZE 15 * next time the ABI changes 16 */ 17 char name[RTE_MEMZONE_NAMESIZE] __rte_cache_aligned; /**< Name of the ring. */ 18 int flags; /**< Flags supplied at creation. */ 19 const struct rte_memzone *memzone; 20 /**< Memzone, if any, containing the rte_ring */ 21 uint32_t size; /**< Size of ring. */ 22 uint32_t mask; /**< Mask (size-1) of ring. */ 23 uint32_t capacity; /**< Usable size of ring */ 24 25 char pad0 __rte_cache_aligned; /**< empty cache line */ 26 27 /** Ring producer status. */ 28 struct rte_ring_headtail prod __rte_cache_aligned; 29 char pad1 __rte_cache_aligned; /**< empty cache line */ 30 31 /** Ring consumer status. */ 32 struct rte_ring_headtail cons __rte_cache_aligned; 33 char pad2 __rte_cache_aligned; /**< empty cache line */ 34 };
2)環形緩沖區剖析
環形緩沖區支持隊列管理。rte_ring並不是具有無限大小的鏈表,它具有如下屬性:
- 先進先出(FIFO)
- 最大大小固定,指針存儲在表中
- 無鎖實現
- 多消費者或單消費者出隊操作
- 多生產者或單生產者入隊操作
- 批量出隊 - 如果成功,將指定數量的元素出隊,否則什么也不做
- 批量入隊 - 如果成功,將指定數量的元素入隊,否則什么也不做
- 突發出隊 - 如果指定的數目出隊失敗,則將最大可用數目對象出隊
- 突發入隊 - 如果指定的數目入隊失敗,則將最大可入隊數目對象入隊
相比於鏈表,這個數據結構的優點如下:
- 更快;只需要一個sizeof(void *)的Compare-And-Swap指令,而不是多個雙重比較和交換指令
- 與完全無鎖隊列像是
- 適應批量入隊/出隊操作。 因為指針是存儲在表中的,應i多個對象的出隊將不會產生於鏈表隊列中一樣多的cache miss。 此外,批量出隊成本並不比單個對象出隊高。
缺點:
- 大小固定
- 大量ring相比於鏈表,消耗更多的內存,空ring至少包含n個指針。
數據結構中存儲的生產者和消費者頭部和尾部指針顯示了一個簡化版本的ring。
3)linux 無鎖緩沖區
參考:https://lwn.net/Articles/340400/
4)ring buffer解析
本節介紹ring buffer的運行方式。 Ring結構有兩組頭尾指針組成,一組被生產者調用,一組被消費者調用。 以下將簡單稱為 prod_head、prod_tail、cons_head 及 cons_tail。
每個圖代表了ring的簡化狀態,是一個循環緩沖器。 本地變量的內容在圖上方表示,Ring結構的內容在圖下方表示。
4.1) 單生產者入隊
本節介紹了一個生產者向隊列添加對象的情況。 在本例中,只有生產者頭和尾指針(prod_head and prod_tail)被修改,只有一個生產者。
初始狀態是將prod_head 和 prod_tail 指向相同的位置。
第一步:
首先, ring->prod_head 和 ring->cons_tail*復制到本地變量中。 *prod_next 本地變量指向下一個元素,或者,如果是批量入隊的話,指向下幾個元素。
如果ring中沒有足夠的空間存儲元素的話(通過檢查cons_tail來確定),則返回錯誤。
第二步:
第二步是在環結構中修改 ring->prod_head,以指向與prod_next相同的位置。
指向待添加對象的指針被復制到ring中。
第三步:
一旦將對象添加到ring中,ring結構中的 ring->prod_tail 將被修改,指向與 ring->prod_head 相同的位置。 入隊操作完成。
4.2)單消費者出列
第一步
首先,將 ring->cons_head 和 ring->prod_tail*復制到局部變量中。 *cons_next 本地變量指向表的下一個元素,或者在批量出隊的情況下指向下幾個元素。
如果ring中沒有足夠的對象用於出隊(通過檢查prod_tail),將返回錯誤。
第二步:
第二步是修改ring結構中 ring->cons_head,以指向cons_next相同的位置。
指向出隊對象(obj1) 的指針被復制到用戶指定的指針中。
第三步:
最后,ring中的ring->cons_tail被修改為指向ring->cons_head相同的位置。 出隊操作完成
4.3 多生產者
本節說明兩個生產者同時向ring中添加對象的情況。 在本例中,僅修改生產者頭尾指針(prod_head and prod_tail)。
初始狀態是將prod_head 和 prod_tail 指向相同的位置。
4.3.1. 多生產者入隊第一步
在生產者的兩個core上, ring->prod_head 及 ring->cons_tail 都被復制到局部變量。 局部變量prod_next指向下一個元素,或者在批量入隊的情況下指向下幾個元素。
如果ring中沒有足夠的空間用於入隊(通過檢查cons_tail),將返回錯誤。

Fig. 4.9 Multiple producer enqueue first step
4.3.2. 多生產者入隊第二步
第二步是修改ring結構中 ring->prod_head ,來指向prod_next相同的位置。 此操作使用比較和交換(CAS)指令,該指令以原子操作的方式執行以下操作:
- 如果ring->prod_head 與本地變量prod_head不同,則CAS操作失敗,代碼將在第一步重新啟動。
- 否則,ring->prod_head設置為本地變量prod_next,CAS操作成功並繼續下一步處理。
在圖中,core1執行成功,core2重新啟動。

Fig. 4.10 Multiple producer enqueue second step
4.3.3. 多生產者入隊第三步
Core 2的CAS操作成功重試。
Core 1更新一個對象(obj4)到ring上。Core 2更新一個對象(obj5)到ring上

Fig. 4.11 Multiple producer enqueue third step
4.3.4. 多生產者入隊地四步
每個core現在都想更新 ring->prod_tail。 只有ring->prod_tail等於prod_head本地變量,core才能更新它。 當前只有core 1滿足,操作在core 1上完成。

Fig. 4.12 Multiple producer enqueue fourth step
4.3.5. 多生產者入隊最后一步
一旦ring->prod_tail被core 1更新完,core 2也滿足條件,允許更新。 Core 2上也完成了操作。

Fig. 4.13 Multiple producer enqueue last step
4.4.4. 32-bit取模索引
在前面的途中,prod_head, prod_tail, cons_head 和 cons_tail索引由箭頭表示。 但是,在實際實現中,這些值不會假定在0和 size(ring)-1 之間。 索引值在 0 ~ 2^32 -1之間,當我們訪問ring本身時,我們屏蔽他們的值。 32bit模數也意味着如果溢出32bit的范圍,對索引的操作將自動執行2^32 模。
以下是兩個例子,用於幫助解釋索引值如何在ring中使用。
Note
為了簡化說明,使用模16bit操作,而不是32bit。 另外,四個索引被定義為16bit無符號整數,與實際情況下的32bit無符號數相反。

Fig. 4.14 Modulo 32-bit indexes - Example 1
這個ring包含11000對象。

Fig. 4.15 Modulo 32-bit indexes - Example 2