前言
RCU 是(shi)一種(zhong)內(nei)核(he)同步機制,在2002年10月加入到 Linux 內(nei)核(he)中。它屬(shu)于(yu)“無鎖編程”的范疇,實(shi)現的功能類(lei)似于(yu)“讀(du)寫鎖”。但是(shi)與“讀(du)寫鎖”相(xiang)比,RCU有著顯著的優(you)勢:
- 讀者性能極高。特別是當RCU以QSBR方式運行時,讀者同步開銷為0。
- 無競爭、無阻塞。讀者寫者之間、讀者讀者之間均不互相阻塞,也沒有死鎖、活鎖問題。
當(dang)(dang)然RCU也(ye)有(you)其劣勢(shi):當(dang)(dang)寫(xie)(xie)(xie)操(cao)作十分頻繁時(shi),寫(xie)(xie)(xie)者開(kai)銷(xiao)較大(da)。雖然寫(xie)(xie)(xie)者不(bu)會被(bei)阻塞或(huo)者出現“寫(xie)(xie)(xie)饑餓”情況,但是當(dang)(dang)有(you)大(da)量(liang)寫(xie)(xie)(xie)操(cao)作時(shi),寫(xie)(xie)(xie)者的綜合開(kai)銷(xiao)不(bu)亞(ya)于,甚至高于傳統的讀寫(xie)(xie)(xie)鎖。另外,RCU不(bu)支持多寫(xie)(xie)(xie)者并發。如果(guo)有(you)多個寫(xie)(xie)(xie)者的話,寫(xie)(xie)(xie)者之間需(xu)要加互斥鎖(或(huo)自(zi)旋鎖)。
URCU是RCU的用戶態版本,目前已經在各種用戶態網關中廣泛應用。參考://github.com/urcu/userspace-rcu
DPDK做為用戶(hu)態(tai)數據面(mian)開發利器,在DPDK19中(zhong)首(shou)次引入(ru)。與URCU庫(ku)相比,它更適合DPDK的PMD模(mo)型,使用起來更簡便。
注意:本文旨在向讀者初步介紹DPDK-RCU庫的使用方式,并假定讀者對“鎖”、“RCU”、“URCU”、“臨界區”、“寬限區”等概念有了充分的了解。此外,本文大部分內容翻譯自DPDK-RCU文檔。其版權歸原作者所有。原文鏈接://doc.dpdk.org/guides/index.html#
DPDK-RCU庫的特點
-
DPDK RCU庫(ku) 允許writer不被阻塞去干別的事情,從(cong)而盡(jin)量縮短臨(lin)界(jie)區(減少靜默狀態匯報),并盡(jin)量延長寬限期(qi)(避免刪除操作積壓)。
-
PMD模型(xing)(xing)的(de)循環間隔(ge)十分適合做靜默期,并且(qie)這個模型(xing)(xing)的(de)臨界區很長,可以將reader的(de)開銷(xiao)降至最低。
-
與傳統(tong)的RCU機制(zhi)不同,DPDK RCU庫支持QS變量(liang)(類似于鎖實例),可以(yi)為每個(ge)需要(yao)保護(hu)的臨(lin)界(jie)區創建一(yi)個(ge)QS變量(liang),可以(yi)更(geng)好地跟(gen)蹤每一(yi)個(ge)寬限期的結束。(傳統(tong)的RCU沒(mei)有QS變量(liang))
DPDK-RCU的使用步驟
-
初始(shi)化(hua)QS,使(shi)用(yong)rte_rcu_qsbr_get_memsize(reader線程的最大(da)數目(mu))為QS變(bian)量分配內存(cun)(cun)(因為writer需(xu)要跟蹤每個(ge)reader的經(jing)靜默狀態,所以分配內存(cun)(cun)的大(da)小和reader的數目(mu)有關)。
-
使(shi)用rte_rcu_qsbr_init()初始化(hua)QS變量。
-
每一個線程(cheng)需要有一個線程(cheng)ID,可以使用lcore_id代替。
-
每一個讀(du)者(zhe)需要調用rte_rcu_qsbr_thread_register()將自(zi)己(ji)注冊為讀(du)者(zhe)(任(ren)意(yi)讀(du)線程(cheng)都行,不一定(ding)是worker)。
-
讀線程(cheng)還(huan)需(xu)要調用rte_rcu_qsbr_thread_online(),然后才(cai)能匯報自己的(de)靜默狀態(tai)。
-
在讀線程執行阻(zu)塞(sai)調(diao)用之前(qian),必須(xu)調(diao)用rte_rcu_qsbr_thread_offline()先將自己下線。調(diao)用阻(zu)塞(sai)函數之后,還得調(diao)用rte_rcu_qsbr_thread_online()上線。
-
寫(xie)線(xian)(xian)程調用rte_rcu_qsbr_start()觸發讀(du)線(xian)(xian)程開始匯報自己的靜默狀態。有多個(ge)寫(xie)線(xian)(xian)程的話,其他寫(xie)線(xian)(xian)程也可以調用這(zhe)個(ge)函數。故,這(zhe)個(ge)函數將會返回(hui)一個(ge)token給調用者。
-
寫者必須(xu)調用rte_rcu_qsbr_check()去獲取當前的靜默(mo)狀態(tai)。注(zhu)意參數(shu)就(jiu)是上面返回的token。如果這(zhe)個函(han)數(shu)的返回值指示所有的讀者都已經經歷(li)了靜默(mo)期,那么寫者就(jiu)可以釋放對應的資源(yuan)。
-
rte_rcu_qsbr_start()和rte_rcu_qsbr_check()是lock-free的。所以(yi),多(duo)個寫者可以(yi)同時調用(yong)。
-
觸發報告和查詢(xun)狀態(tai)的(de)(de)分離為寫入(ru)線(xian)程提供了靈活(huo)性(xing),可(ke)以執行(xing)有用的(de)(de)工作(zuo),而不(bu)是阻塞讀(du)取線(xian)程進(jin)入(ru)靜止狀態(tai)或脫機(ji)。這可(ke)以減少持續polling狀態(tai)導致的(de)(de)內(nei)存訪問次數。但是由于資源被稍后釋放,token和要(yao)被釋放資源的(de)(de)引用要(yao)被存起(qi)來以便(bian)于今后的(de)(de)狀態(tai)查詢(xun)。
-
rte_rcu_qsbr_synchronize()函數(shu)將rte_rcu_qsbr_start()和阻塞版本(ben)的rte_rcu_qsbr_check()功能(neng)組(zu)合在(zai)一起。這(zhe)(zhe)個API觸發讀(du)者(zhe)匯報它們的靜默狀態并且poll直到(dao)所有(you)reader進(jin)入靜默狀態或者(zhe)下(xia)線。這(zhe)(zhe)個API不允許(xu)writer在(zai)等(deng)待(dai)的時候(hou)做其他事(shi)情(qing),會在(zai)持(chi)續(xu)的polling中帶(dai)來額外的內存訪(fang)問次數(shu)。但是,好處(chu)在(zai)于這(zhe)(zhe)個API不必存儲(chu)token和需要delete的資源引用(yong)。資源可以在(zai)rte_rcu_qsbr_synchronize()調用(yong)結(jie)束后立即被釋放。
-
讀線程(cheng)必須(xu)調(diao)用rte_rcu_qsbr_thread_offline()和rte_rcu_qsbr_thread_unregister()將自己從匯報狀(zhuang)態的列表(biao)中(zhong)移除。rte_rcu_qsbr_check() API就不(bu)會再等待這(zhe)些線程(cheng)了。
-
讀者必須調用rte_rcu_qsbr_quiescent()指示自(zi)己進入了(le)靜默狀態(tai)。
-
rte_rcu_qsbr_lock()和rte_rcu_qsbr_unlock()是(shi)(shi)空函數(shu),但是(shi)(shi)利于debug,代表(biao)臨界區(qu)的(de)開(kai)始和結束。rte_rcu_qsbr_quiescent()會檢查是(shi)(shi)否所有的(de)lock都unlock了。
DPDK-RCU的資源回收框架
Lock-free算法給應用(yong)的(de)資源回收帶來了額外的(de)負(fu)擔。當一個寫者(zhe)刪除(chu)數據結構的(de)條目時,這個寫者(zhe):
- 必須開始寬限期
- 必須將要被刪除資源的引用存放在FIFO中
- 應該檢查所有讀者經歷了寬限期并刪除資源
若干API被提供出(chu)來(lai)用(yong)(yong)(yong)以(yi)(yi)(yi)幫助這個過(guo)程。寫者可(ke)以(yi)(yi)(yi)調(diao)(diao)用(yong)(yong)(yong)函數rte_rcu_qsbr_dq_create()來(lai)建立用(yong)(yong)(yong)以(yi)(yi)(yi)存放將要(yao)刪除資(zi)源(yuan)的引用(yong)(yong)(yong)。使用(yong)(yong)(yong)rte_rcu_qsbr_dq_enqueue()將資(zi)源(yuan)存放進(jin)FIFO。如果(guo)(guo)FIFO滿了,rte_rcu_qsbr_dq_enqueue會(hui)在enqueue之前(qian)回(hui)收(shou)資(zi)源(yuan)。它還將定期回(hui)收(shou)資(zi)源(yuan),以(yi)(yi)(yi)防止先(xian)進(jin)先(xian)出(chu)制過(guo)于(yu)龐大。如果(guo)(guo)寫者用(yong)(yong)(yong)盡了資(zi)源(yuan),寫者可(ke)以(yi)(yi)(yi)調(diao)(diao)用(yong)(yong)(yong)rte_rcu_qsbr_dq_reclaim API去(qu)回(hui)收(shou)資(zi)源(yuan)。rte_rcu_qsbr_dq_delete用(yong)(yong)(yong)于(yu)在關機時回(hui)收(shou)所有資(zi)源(yuan)并將FIFO刪除 。
但是,如果將這個資源回(hui)收過(guo)程(cheng)(cheng)集成(cheng)到無鎖(suo)數(shu)據結構(gou)庫中(zhong),它將對(dui)應(ying)(ying)用程(cheng)(cheng)序隱藏這種(zhong)復雜性,并使應(ying)(ying)用程(cheng)(cheng)序更容(rong)易采用無鎖(suo)算法。
在任(ren)何DPDK應(ying)用中(zhong),使用QSBR的資源回收(shou)過程可以被(bei)分為4部(bu)分:
- 初始化
- 匯報靜默狀態
- 回收資源
- 關機
這里(li)的(de)(de)設(she)計建議(yi)是將(jiang)這個(ge)過程(cheng)的(de)(de)不同部分分配給(gei)客(ke)戶(hu)端(duan)庫和應(ying)用(yong)。術語(yu)“客(ke)戶(hu)端(duan)庫”指的(de)(de)是lock-free的(de)(de)數據(ju)結構(gou),例如rte_hash, rte_lpm,等等。在DPDK里(li)面或者在外部的(de)(de)庫。術語(yu)“應(ying)用(yong)”指的(de)(de)是使用(yong)DPDK的(de)(de)數據(ju)包處理程(cheng)序,例如L3_Forwarding, OVS,VPP,etc.
應(ying)用需要掌管“初始化”和“靜默狀(zhuang)態匯報”,所以:
- 應用需要建立RCU變量并注冊讀線(xian)程去匯報靜(jing)默狀態
- 應用和客(ke)戶(hu)端庫(ku)必須注冊相同的(de)RCU變量(liang)。
- 應(ying)用(yong)中的讀(du)線程需要匯報靜(jing)默狀態。這允許(xu)應(ying)用(yong)去(qu)控制臨(lin)界區的長度,以及應(ying)用(yong)匯報靜(jing)默期的頻(pin)率。
客(ke)戶(hu)端(duan)庫(ku)(ku)需(xu)要掌控這個過程的“資源回收(shou)”部分(fen)。客(ke)戶(hu)端(duan)庫(ku)(ku)將使用寫入線程上下文來執(zhi)行內(nei)存回收(shou)算法。所以:
- 客戶端庫應該提供API去注冊即將使用的RCU變量。它需要調用rte_rcu_qsbr_dq_create()去建立存放已經刪除的條目的索引的FIFO。
- 客戶端庫需要使用rte_rcu_qsbr_dq_enqueue()去將刪除的資源放入FIFO隊列并開始寬限期。
- 當客戶端添加條目時用盡了資源,它需要調用rte_rcu_qsbr_dq_reclaim()去回收回收資源并再次嘗試分配資源。
“shutdown”該(gai)(gai)過程應該(gai)(gai)在(zai)應用(yong)和客戶(hu)端(duan)庫之間共享。
- 應用需要確保讀線程沒有正在使用共享數結構。在調用客戶端庫的“shutdown”功能之前,需要從QSBR變量中unregister讀線程。
- 客戶端庫需要調用rte_rcu_qsbr_dq_delete去回收任何需要回收的資源,并釋放FIFO。
將資源回收集成到客戶端庫可以消除應用的負(fu)擔,并使應用可以方便(bian)地使用lock-free算法。
- 應用不需要專門的線程去回收資源。內存回收做為寫線程的一部分而發生,并且不會影響性能。
- 客戶端可以更好地控制資源。例如:客戶端在耗盡資源后可以嘗試回收。