一文深入分析|RCU原理
Linux 內(nèi)核設(shè)計了多種鎖機(jī)制,比如?讀寫鎖
、自旋鎖
?和?信號量
?等。為什么要設(shè)計這么多鎖機(jī)制呢?這是因為不同的鎖機(jī)制適用于不同的場景,比如?讀寫鎖
?適用于讀多寫少的場景;而?信號量
?適用于進(jìn)程長時間占用鎖,并且允許上下文切換的場景。
本文主要介紹一種 Linux 內(nèi)核中性能非常高的鎖機(jī)制:RCU鎖機(jī)制
。
RCU
?是?Read Copy Update
?的縮寫,中文意思是?讀取
、復(fù)制
、更新
。RCU鎖機(jī)制 就是通過讀取、復(fù)制和更新這三個操作來實現(xiàn)鎖功能。在介紹?RCU鎖
?之前,我們先來看看下面的實例。
假如有線程 A 和線程 B 同時執(zhí)行?foo_read()
,而另線程 C 執(zhí)行?foo_update()
,那么會出現(xiàn)以下幾種情況:
線程 A 和線程 B 同時讀取到舊的 gbl_foo 的指針。
線程 A 和線程 B 同時讀取到新的 gbl_foo 的指針。
線程 A 和線程 B 有一個讀取到新的 gbl_foo 的指針,另外一個讀取到舊的 gbl_foo 的指針。
如果線程 A 或線程 B 在讀取舊的 gbl_foo 數(shù)據(jù)還沒完成時,線程 C 釋放了舊的 gbl_foo 指針,那么將會導(dǎo)致程序奔潰。
也就是說,在不加鎖的情況下,對公共數(shù)據(jù)的訪問是危險的。當(dāng)然,我們可以使用?讀寫鎖
、信號量
?或者?自旋鎖
?來對公共數(shù)據(jù)進(jìn)行保護(hù)。但這些鎖都有各自的弊端,比如:
讀寫鎖
:對于寫操作較多的場景,性能會非常差。信號量
:上鎖失敗的進(jìn)程將會切換上下文,從而導(dǎo)致系統(tǒng)的性能下降。自旋鎖
:獲得鎖的 CPU 將會阻塞其他 CPU 的允許,從而導(dǎo)致系統(tǒng)的并行能力下降。
那么有沒有一種鎖機(jī)制,對系統(tǒng)的性能影響不大的呢?所以,Linux 內(nèi)核黑客們就創(chuàng)造出?RCU鎖
。
RCU鎖原理
如果能夠保證所有使用某個公共數(shù)據(jù)的線程不再使用它,那么就可以安全刪除此公共數(shù)據(jù)。
1. 寬限期
在上面的例子中,如果能夠保證線程 A 和線程 B 不再使用舊數(shù)據(jù),那么線程 C 就能安全刪除舊數(shù)據(jù)。
如下圖所示(舊數(shù)據(jù)對應(yīng)對象A,新數(shù)據(jù)對應(yīng)對象B):

從上圖的時間線可以看出,線程 A 和線程 B 從 glb_foo 指針獲取的都是對象 A 的引用。
提示:因為 glb_foo 指針在時間點 B 才被替換成對象 B,而線程 A 和線程 B 都是在時間點 B 前獲取 glb_foo 指針指向的對象,所以它們獲取到的都是對象 A 的引用。
而在?安全點
?后,線程 A 和線程 B 便不再使用舊數(shù)據(jù)(對象A)。所以此時,線程 C 便可以安全釋放舊數(shù)據(jù)(對象A)。
線程 A 和線程 B 使用舊數(shù)據(jù)的這段期間,被稱為?寬限期
。如下圖所示:

所以,RCU鎖
?的核心思想就是怎么確定?寬限期
。因為確定寬限期后,就可以隨心所欲地釋放舊數(shù)據(jù)。
2. 寬限期確認(rèn)
RCU鎖
?的原理雖然比較簡單,但是實現(xiàn)卻有點小復(fù)雜,主要是因為?寬限期
?的確定比較麻煩。
為了能夠確認(rèn)?寬限期
,使用 RCU 鎖時有以下限制:
使用 RCU 鎖前,必須禁止內(nèi)核搶占。
在 RCU 鎖保護(hù)的臨界區(qū)中,不能使用可能觸發(fā)調(diào)度的函數(shù)(如不能調(diào)用 alloc_pages 函數(shù))。
由于在 RCU 臨界區(qū)是禁止調(diào)度的,所以如果 CPU 發(fā)生了調(diào)度,就可以確定當(dāng)前線程已經(jīng)退出了臨界區(qū)(也就是說當(dāng)前線程不再引用舊對象)。如果所有的 CPU 都至少發(fā)生過一次調(diào)度,那么也就說明沒有任何線程引用舊對象,此時就可以安全釋放舊對象了。
所以,RCU 鎖的核心原理是:在釋放舊對象前,必須等待所有 CPU 核心至少調(diào)度一次。如下代碼所示:
foo_update()
?函數(shù)釋放舊對象的步驟如下:
使用新對象替換舊對象,在替換前必須使用自旋鎖進(jìn)行保護(hù),避免多個 CPU 同時修改 gbl_foo 指針的值。
等待所有 CPU 核心至少調(diào)度一次。
由于所有 CPU 核心都至少調(diào)度過一次,那么可以確認(rèn)現(xiàn)在沒有線程引用舊對象,所以可以安全釋放舊對象。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ??


3. RCU臨界區(qū)
通過前面的分析可知,在 RCU 臨界區(qū)中是不能發(fā)生調(diào)度的。要保證臨界區(qū)不發(fā)生調(diào)度,首先要確保在臨界區(qū)中不能調(diào)用可能觸發(fā)調(diào)度的函數(shù),如:alloc_pages()
。這點需要 RCU 使用者自己保證。
另外一點要保證的是,內(nèi)核不能發(fā)生搶占,這點可以通過調(diào)用?preempt_disable()
?函數(shù)實現(xiàn)。內(nèi)核定義了一個名為?rcu_read_lock()
?的宏,如下所示:
可以看出,?rcu_read_lock()
?宏其實就是?preempt_disable()
?函數(shù)的別名。所以,使用 RCU 鎖時,可以使用?rcu_read_lock()
?宏對臨界區(qū)進(jìn)行保護(hù)。
當(dāng)退出臨界區(qū)時,需要調(diào)用?rcu_read_unlock()
?把內(nèi)核搶占打開。rcu_read_unlock()
?的定義如下:
可以看出,rcu_read_unlock()
?宏就是?preempt_enable()
?的別名。
所以,當(dāng)我們使用 RCU 鎖對臨界區(qū)進(jìn)行保護(hù)時,必須將需要保護(hù)的代碼放置在?rcu_read_lock()
?和?rcu_read_unlock()
?之間,如下所示:
原文作者:Linux內(nèi)核那些事
