萬字講解linux kermel RCU以及讀寫鎖
信號量有一個很明顯的缺點,沒有區(qū)分臨界區(qū)的讀寫屬性,讀寫鎖允許多個線程進(jìn)程并發(fā)的訪問臨界區(qū),但是寫訪問只限于一個線程,在多處理器系統(tǒng)中允許多個讀者訪問共享資源,但是寫者有排他性,讀寫鎖的特性如下:允許多個讀者同時訪問臨界區(qū),但是同一時間不能進(jìn)入;同一時刻只允許一個寫者進(jìn)入臨界區(qū);讀者和寫者不能同時進(jìn)入臨界區(qū)。讀寫鎖也有關(guān)閉中斷和下半部的版本。
RCU:read-copy-update 。。。。。。。。。。。。。。。。。。。。
問題:rcu相比讀寫鎖,解決了什么問題? rcu的基本原理?
1、由于內(nèi)核中spinlock mutex 等都使用了原子操作指令,即原子的訪問內(nèi)存,但是當(dāng)多cpu 競爭訪問臨界區(qū)時會讓cpu的cache命中率下降,性能下降。同時讀寫鎖有個缺陷,讀者和寫者不能同時存在。
rcu實現(xiàn)的目標(biāo)就是要解決這個問題,為了使線程同步開銷小。不需要原子操作以及內(nèi)存屏障而訪問數(shù)據(jù),把同步的問題交給寫者線程,寫者線程等待所有的讀者線程完成后才會吧舊數(shù)據(jù)銷毀。當(dāng)有多個寫者線程存在時,需要額外的保護機制。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ??


原理
RCU原理:簡單理解為 記錄了所有指向共享數(shù)據(jù)的指針使用者,當(dāng)要修改共享數(shù)據(jù)時,先創(chuàng)建一個副本,在副本中修改。所有讀者離開臨界區(qū)后,指針指向新的修改副本后的地方,并且刪除舊數(shù)據(jù)。
官方描述:RCU實際上是一種改進(jìn)的rwlock,讀者幾乎沒有什么同步開銷,它不需要鎖,不使用原子指令,因此不會導(dǎo)致鎖競爭,內(nèi)存延遲以及流水線停滯。不需要鎖也使得使用更容易,因為死鎖問題就不需要考慮了。
寫者的同步開銷比較大,它需要延遲數(shù)據(jù)結(jié)構(gòu)的釋放,復(fù)制被修改的數(shù)據(jù)結(jié)構(gòu),它也必須使用某種鎖機制同步并行的其它寫者的修改操作。
讀者必須提供一個信號給寫者以便寫者能夠確定數(shù)據(jù)可以被安全地釋放或修改的時機。
有一個專門的垃圾收集器來探測讀者的信號,一旦所有的讀者都已經(jīng)發(fā)送信號告知它們都不在使用被RCU保護的數(shù)據(jù)結(jié)構(gòu),垃圾收集器就調(diào)用回調(diào)函數(shù)完成最后的數(shù)據(jù)釋放或修改操作。
目前在內(nèi)核中鏈表使用RCU較多。
在經(jīng)典RCU中,RCU讀側(cè)臨界部分由rcu_read_lock() 和rcu_read_unlock()界定,它們可以嵌套。
對應(yīng)的同步更新原語為synchronize_rcu(),還有同義的synchronize_net(),等待當(dāng)前正執(zhí)行的RCU讀側(cè)聞臨界部分運行完成。等待的時間稱為“寬限期”。
異步更新側(cè)原語call_rcu()在寬限期之后觸發(fā)指定的函數(shù),如:call_rcu(p,f)調(diào)用觸發(fā)回調(diào)函數(shù)f(p)。有些情況,如:當(dāng)卸載使用call_rcu()的模塊,必須等待所有RCU回調(diào)函數(shù)完成,原語rcu_barrier()起該作用。 在“RCU BH”列中,rcu_read_lock_bh() 和rcu_read_unlock_bh()界定讀側(cè)臨界部分,call_rcu_bh()在寬限期后觸發(fā)指定的函數(shù)。注意:RCU BH沒有同步接口synchronize_rcu_bh(),如果需要,用戶很容易添加同步接口函數(shù)。
直接操作指針的原語rcu_assign_pointer()和rcu_dereference()用于創(chuàng)建RCU保護的非鏈表數(shù)據(jù)結(jié)構(gòu),如:數(shù)組和樹
NOTE:讀者 在訪問被RCU保護的共享數(shù)據(jù)期間不能被阻塞,這是RCU機制得以實現(xiàn)的一個基本前提,也就說當(dāng)讀者在引用被RCU保護的共享數(shù)據(jù)期間,讀者所在的CPU不能發(fā)生上下文切換,spinlock和rwlock都需要這樣的前提。 寫者 在訪問被RCU保護的共享數(shù)據(jù)時不需要和讀者競爭任何鎖,只有在有多于一個寫者的情況下需要獲得某種鎖以與其他寫者同步。寫者修改數(shù)據(jù)前首先拷貝一個被修改元素的副本,然后在副本上進(jìn)行修改,修改完畢后它向垃圾回收器注冊一個回調(diào)函數(shù)以便在適當(dāng)?shù)臅r機執(zhí)行真正的修改操作。等待適當(dāng)時機的這一時期稱為寬限期(grace period),而CPU發(fā)生了上下文切換稱為經(jīng)歷一個quiescent state,grace period就是所有CPU都經(jīng)歷一次quiescent state所需要的等待的時間。垃圾收集器就是在grace period之后調(diào)用寫者注冊的回調(diào)函數(shù)來完成真正的數(shù)據(jù)修改或數(shù)據(jù)釋放操作的。
在使用RCU時,對共享資源的訪問在大部分時間應(yīng)該是只讀的,寫訪問應(yīng)該相對較少,因為寫訪問多了必然相對于其他鎖機制而已更占系統(tǒng)資源,影響效率。其次是讀者在持有rcu_read_lock(RCU讀鎖定函數(shù))的時候,不能發(fā)生進(jìn)程上下文切換,否則,因為寫者需要等待讀者完成方可進(jìn)行,則此時寫者進(jìn)程也會一直被阻塞,影響系統(tǒng)的正常運行。再次寫者執(zhí)行完畢后需要調(diào)用回調(diào)函數(shù),此時發(fā)生上下文切換,當(dāng)前進(jìn)程進(jìn)入睡眠,則系統(tǒng)將一直不能調(diào)用回調(diào)函數(shù),更槽糕的是,此時其它進(jìn)程若再去執(zhí)行共享的臨界區(qū),必然造成一定的錯誤。最后一點是受RCU機制保護的資源必須是通過指針訪問。因為從RCU機制上看,幾乎所有操作都是針對指針數(shù)據(jù)的;
同步函數(shù)最為重要,即synchronize_rcu()。讀者函數(shù)的實質(zhì)其實很簡單:禁止搶占,也就是說在RCU期間不允許發(fā)生進(jìn)程上下文切換,原因上述已提及,即是寫者需要等待讀者完成方可進(jìn)行,則此時寫者進(jìn)程也會一直被阻塞,影響系統(tǒng)的正常運行等,故而不允許在RCU期間發(fā)生進(jìn)程上下文切換
關(guān)于寫者函數(shù),主要就是call_rcu和call_rcu_bh兩個函數(shù)。其中call_rcu能實現(xiàn)的功能是它不會使寫者阻塞,因而它可在中斷上下文及軟中斷使用,該函數(shù)將函數(shù)func掛接到RCU的回調(diào)函數(shù)鏈表上,然后立即返回,讀者函數(shù)中提及的synchronize_rcu()函數(shù)在實現(xiàn)時也調(diào)用了該函數(shù)。而call_rcu_bh函數(shù)實現(xiàn)的功能幾乎與call_rcu完全相同,唯一的差別是它將軟中斷的完成當(dāng)作經(jīng)歷一個quiescent state(靜默狀態(tài),本節(jié)一開始有提及這個概念), 因此若寫者使用了該函數(shù),那么讀者需對應(yīng)的使用rcu_read_lock_bh() 和rcu_read_unlock_bh()。
使用rcu_read_lock_bh() 和rcu_read_unlock_bh()函數(shù)的原因是由于call_rcu_bh函數(shù)不會使寫者阻塞,可在中斷上下文及軟中斷使用。這表明此時系統(tǒng)中的中斷和軟中斷并沒有被關(guān)閉。那么寫者在調(diào)用call_rcu_bh函數(shù)訪問臨界區(qū)時,RCU機制下的讀者也能訪問臨界區(qū)。此時對于讀者而言,它若是需要讀取臨界區(qū)的內(nèi)容,它必須把軟中斷關(guān)閉,以免讀者在當(dāng)前的進(jìn)程上下文過程中被軟中斷打斷(上述內(nèi)容提過軟中斷可以打斷當(dāng)前的進(jìn)程上下文)。而rcu_read_lock_bh() 和rcu_read_unlock_bh()函數(shù)的實質(zhì)是調(diào)用local_bh_disable()和local_bh_enable()函數(shù),顯然這是實現(xiàn)了禁止軟中斷和使能軟中斷的功能。
另外在Linux源碼中關(guān)于call_rcu_bh函數(shù)的注釋中還明確說明了如果當(dāng)前的進(jìn)程是在中斷上下文中,則需要執(zhí)行rcu_read_lock()和rcu_read_unlock(),結(jié)合這兩個函數(shù)的實現(xiàn)實質(zhì)表明它實際上禁止或使能內(nèi)核的搶占調(diào)度,原因不言而喻,避免當(dāng)前進(jìn)程在執(zhí)行讀寫過程中被其它進(jìn)程搶占。同時內(nèi)核注釋還表明call_rcu_bh這個接口函數(shù)的使用條件是在大部分的讀臨界區(qū)操作發(fā)生在軟中斷上下文中,原因還是需從它實現(xiàn)的功能出發(fā),相信很容易理解,主要是要從執(zhí)行效率方面考慮。
static inline void rcu_read_lock_bh(void); static inline void rcu_read_unlock_bh(void);
這個變種只在修改是通過 call_rcu_bh進(jìn)行的情況下使用,因為 call_rcu_bh將把 softirq 的執(zhí)行完畢也認(rèn)為是一個 quiescent state,因此如果修改是通過 call_rcu_bh 進(jìn)行的,在進(jìn)程上下文的讀端臨界區(qū)必須使用這一變種
每一個 CPU 維護兩個數(shù)據(jù)結(jié)構(gòu) rcu_sched_data,rcu_bh_data,它們用于保存回調(diào)函數(shù)。函數(shù)call_rcu和函數(shù)call_rcu_bh用于注冊回調(diào)函數(shù),前者把回調(diào)函數(shù)注冊到rcu_sched_data,而后者則把回調(diào)函數(shù)注冊到rcu_bh_data,在每一個數(shù)據(jù)結(jié)構(gòu)上,回調(diào)函數(shù)被組成一個鏈表,先注冊的排在前頭,后注冊的排在末尾;時鐘中斷處理函數(shù)(update_process_times)調(diào)用函數(shù)rcu_check_callbacks
函數(shù)rcu_check_callbacks首先檢查該CPU是否經(jīng)歷了一個quiescent state,如果(或):
當(dāng)前進(jìn)程運行在用戶態(tài);
當(dāng)前進(jìn)程為idle且當(dāng)前不處在運行softirq狀態(tài),也不處在運行IRQ處理函數(shù)的狀態(tài);
該CPU已經(jīng)經(jīng)歷了一個quiescent state,因此通過調(diào)用函數(shù)rcu_sched_qs和rcu_bh_qs標(biāo)記該CPU的數(shù)據(jù)結(jié)構(gòu)rcu_sched_data和rcu_bh_data的標(biāo)記字段passed_quiesc,以記錄該CPU已經(jīng)經(jīng)歷一個quiescent state。
否則,如果當(dāng)前不處在運行softirq狀態(tài),那么,只標(biāo)記該CPU的數(shù)據(jù)結(jié)構(gòu)rcu_bh_data的標(biāo)記字段passed_quiesc,以記錄該CPU已經(jīng)經(jīng)歷一個quiescent state。注意,該標(biāo)記只對rcu_bh_data有效。
然后,函數(shù)rcu_check_callbacks將調(diào)用開啟RCU_SOFTIRQ。
synchronize_rcu()在RCU中是一個最核心的函數(shù),它用來等待之前的讀者全部退出。
在完整的寬限期結(jié)束后,即在所有當(dāng)前正在執(zhí)行的RCU讀取端臨界區(qū)完成之后,控制權(quán)會在一段時間后返回給調(diào)用者。

原文作者:codestacklinuxer
