DPDK 無鎖隊列Ring Library原理(學習筆記)


參考自DPDK官方文檔原文:http://doc.dpdk.org/guides-20.02/prog_guide/ring_lib.html

針對自己的理解做了一些輔助解釋。

1 前置知識

1.1 CAS

學習無鎖隊列前先看一個基本概念,CAS原子指令操作。

CAS(Compare and Swap,比較並替換)原子指令,用來保障數據的一致性。

指令有三個參數,當前內存值V、舊的預期值A、更新的值B,當且僅當預期值A和內存值V相同時,將內存值修改為B並返回true,否則什么都不做,並返回false。

在DPDK中封裝后的函數如下:

rte_atomic32_cmpset(&r->prod.head, *old_head, *new_head)

&r->prod.head指向當前內存值,*old_head為執行該操作前將r->prod.head存儲到臨時變量的值,*new_head為即將更新的值。

只有r->prod.head == *old_head才會將r->prod.head更新為*new_head

1.2 其他ring實現的參考(了解)

1)FreeBSD中Ring實現的參考

在FreeBSD 8.0中添加了以下代碼,並在某些網絡設備驅動程序中使用(至少在Intel驅動程序中):

2)Linux中無鎖Ring的實現

    http://lwn.net/Articles/340400/

2 Ring Library

2.1 介紹

ring是一個有限大小的鏈表,它具有以下屬性:

  • FIFO( First Input First Output)簡單說就是指先進先出
  • 大小固定,指針存儲在表中
  • 無鎖實現
  • 多消費者或單消費者出隊
  • 多生產者或單生產者入隊
  • 批量(Bulk)出隊:如果成功,將指定數量的對象出隊; 否則失敗
  • 批量入庫:如果成功,將指定數量的對象入隊; 否則失敗
  • 爆發(Burst)出隊:如果指定數量的對象無法滿足,則將最大可用數量的對象出隊
  • 爆發入隊:如果指定數量的對象無法滿足,則將最大可用數量的對象入隊

這種數據結構相比於鏈表隊列優勢:

  • 更快:比較void *大小的數據,只需要執行單次CAS指令,而不需要執行2次CAS指令
  • 比完全無鎖隊列簡單
  • 適用於批量入隊/出隊操作。因為指針存儲在表中,多個對象出隊並不會像鏈表隊列那樣產生大量的緩存未命中,此外,多個對象批量出隊不會比單個對象出隊開銷大

缺點如下:

  • 大小固定
  • 它在許多情況下,內存方面的成本比鏈表列表的成本更高。空環至少包含N個指針。

Ring庫的用例包括:

  • DPDK應用之間的信息交互
  • 內存池中的使用

注:

    一個Ring被唯一的名字識別,當嘗試創建兩個名字相同的Ring時,rte_ring_create()函數會在第二次執行時返回NULL。

2.2 Ring實現原理

本節介紹環形緩存的運作方式。ring結構由兩對head,tail組成,一對被生產者使用(cons),一對被消費者使用(prod),

在后續介紹中,r->cons.head和r->cons.tail 分別指向消費者的頭和尾,r->prod.head和r->prod.tail指向生產者的頭和尾。

clipboard

下文中每種圖形代表了環形緩存ring的一個簡單狀態。局部變量在隊列圖形的上方表示(如cons_head,prod_head等都是局部變量),ring結構相關變量在隊列圖形的下方表示(以r->開頭)。

2.2.1 單生產者入隊

本節介紹當單生產者添加一個對象到ring時發生了什么。在這個例子中,僅只有一個生產者,僅只有生產者的head和tail(r->cons.head、r->cons.tail)索引被修改了,在初始狀態, 它們指向相同的位置。

2.2.2.1 入隊第一步

使用局部變量保存r->prod.head 和 r->cons.tail,同時prod_next局部變量指向prod_head的下一個元素,若是批量入隊就指prod_head的下N個元素。假如ring里沒有足夠的空間(通過檢查cons_tail),入隊函數將返回錯誤。

clipboard

2.2.2.2  入隊第二步

修改ring結構體里的r->prod.head 索引,將它指向局部變量prod_next指向的位置。

將“新增對象的指針”(下圖中的obj4)復制到ring里。

clipboard

2.2.2.3 入隊最后一步

一旦添加對象被復制到ring后,ring結構體里的 r->prod.tail索引將指向 r->prod.head的位置,入隊操作完成。

clipboard

2.2.3 單消費者出隊

在這個例子中,僅有一個消費者,僅有消費者的head和tail(r->cons.head 和 r->cons.tail)索引被修改了。

初始狀態, r->cons.head 和 r->cons.tai指向相同的位置。

2.2.2.1 出隊第一步

使用局部變量保存r->cons.head 和 r->prod.tail。 cons_next局部變量指向cons_head的下一個元素,若是批量出隊就指向cons_head的下N個元素。假如ring里沒有足夠的空間(通過檢查prod_tail),出隊函數將返回錯誤。

clipboard

2.2.2.2 出隊第二步

修改ring結構體里的r->cons.head 索引,將它指向局部變量cons_next指向的位置。

將對象的指針(上圖ring中的obj1)復制到用戶傳進來的指針中。

clipboard

2.2.2.3 出隊最后一步

ring結構體中的 ring->cons.tail索引指向和 ring->cons.head,局部變量cons_next相同的位置(obj2的位置)。出隊操作完成。

clipboard

2.2.4 多生產者入隊

在這個例子中,僅有生產者的head和tail(r->prod.head 和 r->prod.tail)被修改了。初始狀態, 它們指向相同的位置。

2.2.4.1 多生產者入隊第一步

在兩個生產者core中(這個core可以理解成同時運行的線程或進程),各自的局部變量都保存r->prod.head 和 r->cons.tail。 各自的局部變量prod_next索引指向r->prod.head的下一個元素,如果是批量入隊,指向下N個元素。

假如ring里沒有足夠的空間(檢查cons_tail獲知),入隊函數將返回錯誤。

2

2.2.4.2 多生產者入隊第二步

修改ring結構體里的r->prod.head 索引,將它指向局部變量prod_next指向的位置。這個操作是通過使用 Compare And Swap (CAS)執行完成的, Compare And Swap (CAS)包含以下原子操作:

  • 如果r->prod.head索引和局部變量prod_head索引不相等,CAS操作失敗,代碼將重新從第一步開始執行。
  • 否則,將r->prod.head索引指向局部變量prod_next的位置,CAS操作成功,繼續下一步處理。

注:涉及到了兩個core同時對r->prod.head讀取,使用了volatile修飾。同樣的prod和cons的2對head和tail都是用了volatile修飾。

在下圖中,生產者core1執行成功,生產者core2重新從第一步開始執行。

1

2.2.4.3 多生產者入隊第三步

生產者core2中CAS指令重試成功,r->prod.head位置被更新。

生產者core1更新對象obj4到ring中,生產者core2更新對象obj5到ring中。

clipboard

2.2.4.4 多生產者入隊第四步

現在每個生產者core都想通過CAS更新 r->prod.tail索引。生產者core代碼中,只有r->prod.tail等於自己局部變量prod_head才能被更新,顯然從上圖中可知,只有生產者core1才能滿足,生產者core1完成了入隊操作。

clipboard

2.2.4.5 多生產者入隊最后一步

一旦生產者core1更新了r->prod.tail后,生產者core2也可以更新r->prod.tail了。至此,生產者core2也完成了入隊操作。

image

注:

1)從修改r->prod.head和r->prod.tail的步驟來看,存在“重試”的動作(代碼里看是通過while循環不斷嘗試),因此雖然說是無鎖,但是在多生產者情況下還是會有競爭。在創建隊列時需要傳入是否多生產者的標記,這個標記一定要正確,否則影響性能或准確性。

2)多消費者情況類似,參考上文可以推導出,這里就不再重復。

2.2.5 關於32位取模索引

在前面的圖例中,prod_head, prod_tail, cons_head 和 cons_tail 都是用箭頭表示的。但在實際的代碼實現中,他們的值並不是0和ring大小減一之間的數值。索引的大小范圍是0—2^32-1,當訪問ring中的數據時,真正的索引等於ring中索引值和掩碼與之后的值。32 bit取模的意思是如果索引操作(加減)的結果的值超出了32 bit數據的范圍,溢出的值忽略,只看省下的位組成的數。

下面的兩個例子幫助解釋索引在ring中如何使用的,為了簡便,例子操作的是16位而不是32位。另外,關鍵的四個索引也被定義成16位的整數,現實代碼實現是用得32位的整數。

例1:ring包含了11000條目

clipboard

例2:ring包含了12536個條目

clipboard

為了便於理解,上面的例子中使用模65536的操作。在真實代碼實現中,這是冗長低效的,但是當結果溢出時是自動完成的。

代碼實現總是將producer 和 consumer保持0—ring大小減1的距離。 這個特性的好處是我們能在兩個32位索引值之間做減法,且差值永遠在0一ring大小減1范圍內:這也是為什么結果溢出不是什么大問題。

在任何時候,已經使用的條目和空閑的條目永遠在0一ring大小減1之間。

uint32_t entries = (prod_tail - cons_head);
uint32_t free_entries = (mask + cons_tail - prod_head);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM