Linux內(nèi)核源碼分析 (A.6)RCU機(jī)制及內(nèi)存優(yōu)化屏障
一、RCU機(jī)制
- 問題:
RCU
英文全稱為Read-Copy-Update
,顧名思義就是讀-拷貝-更新,是Linux
內(nèi)核中重要的同步機(jī)制。Linux
內(nèi)核已有原子操作、讀寫信號(hào)量等鎖機(jī)制,為什么要單獨(dú)設(shè)計(jì)一個(gè)比較復(fù)雜的新機(jī)制?
1、RCU的原理和特點(diǎn)
- RCU的原理
RCU記錄所有指向共享數(shù)據(jù)的指針的使用者,當(dāng)要修改該共享數(shù)據(jù)時(shí),首先創(chuàng)建一個(gè)副本, 在副本中修改。所有讀訪問程都離開讀臨界區(qū)之后,指針指向新的修改后副本的指針,并且刪除舊數(shù)據(jù)。 - RCU的特點(diǎn)
- RCU寫者修改對(duì)象的過(guò)程是:首先復(fù)制生成一個(gè)副本,然后更新這個(gè)副本,最后使用新的對(duì)象替換舊的對(duì)象。在寫者執(zhí)行復(fù)制更新的時(shí)候讀者可以讀數(shù)據(jù)。
- 寫者刪除對(duì)象,必須等到所有訪問被刪除對(duì)象的讀者訪問結(jié)束,才能夠執(zhí)行銷毀操作。RCU關(guān)鍵技術(shù)是怎么判斷所有讀者已經(jīng)完成訪問。等待所有讀者訪問結(jié)束的時(shí)間稱為寬限期(
grace period
) 。 - RCU讀者不并不需要直接與寫者進(jìn)行同步,讀者與寫者也能并發(fā)的執(zhí)行。RCU目標(biāo)最大程序來(lái)減少讀者的開銷。因?yàn)橐步?jīng)常使用于讀者性能要求高的場(chǎng)景。
- RCU優(yōu)點(diǎn):讀者開銷少,不需要獲取任何鎖,不需要執(zhí)行原子指令或內(nèi)存屏障;沒有死鎖問題;沒有優(yōu)先級(jí)反轉(zhuǎn)的問題;沒有內(nèi)存泄露的危險(xiǎn)問題;很好的實(shí)時(shí)延遲操作。
- RCU缺點(diǎn):寫者的同步開銷比較大的,寫者之間需要互斥處理;使用其它同步機(jī)制復(fù)雜。
2、核心API(例中使用RCU保護(hù)指針)
-
假定指針
ptr
指向一個(gè)被RCU
保護(hù)的數(shù)據(jù)結(jié)構(gòu)。直接反引用指針是禁止的,首先必須調(diào)用rcu_dereference(ptr)
,然后反引用返回的結(jié)果。此外,反引用指針并使用其結(jié)果的代碼,需要用rcu_read_lock
和rcu_read_unlock
調(diào)用保護(hù)起來(lái):rcu_read_lock(); /*被反引用的指針不能在rcu_read_lock()和rcu_read_unlock() 保護(hù)的代碼范圍之外使用,也不能用于寫訪問。*/ p = rcu_dereference(ptr); if (p != NULL) { awesome_function(p); } rcu_read_unlock();
-
如果必須修改
ptr
指向的對(duì)象,則需要使用rcu_assign_pointer()
:struct super_duper *new_ptr = kmalloc(...); new_ptr->meaning = xyz; new_ptr->of = 42; new_ptr->life = 23; rcu_assign_pointer(ptr, new_ptr);
按RCU的術(shù)語(yǔ),該操作公布了這個(gè)指針,后續(xù)的讀取操作將看到新的結(jié)構(gòu),而不是原來(lái)的。如果更新可能來(lái)自內(nèi)核中許多地方,那么必須使用普通的同步原語(yǔ)防止并發(fā)的寫操作,如自旋鎖。盡管RCU能保護(hù)讀訪問不受寫訪問的干擾,但它不對(duì)寫訪問之間的相互干擾提供防護(hù)!
-
在新值已經(jīng)公布之后,舊的結(jié)構(gòu)實(shí)例會(huì)怎么樣呢?在所有的讀訪問完成之后,內(nèi)核可以釋放該內(nèi)存,但它需要知道何時(shí)釋放內(nèi)存是安全的。為此,RCU提供了另外兩個(gè)函數(shù)。
-
synchronize_rcu()
等待所有現(xiàn)存的讀訪問完成。在該函數(shù)返回之后,釋放與原指針關(guān)聯(lián)的內(nèi)存是安全的。 -
call_rcu
可用于注冊(cè)一個(gè)函數(shù),在所有針對(duì)共享資源的讀訪問完成之后調(diào)用。這要求將一個(gè)rcu_head
實(shí)例嵌入(不能通過(guò)指針)到RCU保護(hù)的數(shù)據(jù)結(jié)構(gòu)。
該回調(diào)函數(shù)可通過(guò)參數(shù)訪問對(duì)象的struct super_duper { struct rcu_head head; int meaning, of, life; };
rcu_head
成員,進(jìn)而使用container_of
機(jī)制訪問對(duì)象本身。
kernel/rcupdate.cvoid fastcall call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu))
-
3、鏈表操作
-
RCU
不僅能保護(hù)一般的指針,還能保護(hù)內(nèi)核提供的雙鏈表和散列表。以鏈表為例,仍然可以使用標(biāo)準(zhǔn)的鏈表元素。只有在遍歷鏈表、修改和刪除鏈表元素時(shí),必須調(diào)用標(biāo)準(zhǔn)函數(shù)的RCU
變體(附加_rcu
后綴)。
<list.h>/*將新的鏈表元素new添加到表頭為head的鏈表頭部*/ static inline void list_add_rcu(struct list_head *new, struct list_head *head) /*將新的鏈表元素new添加到表頭為head的鏈表尾部*/ static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head) /*從鏈表刪除鏈表元素entry*/ static inline void list_del_rcu(struct list_head *entry) /*將鏈表元素old替換為new*/ static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
此外,
list_for_each_rcu
允許遍歷鏈表的所有元素。而list_for_each_rcu_safe
甚至對(duì)于刪除鏈表元素也是安全的。這兩個(gè)操作都必須通過(guò)一對(duì)rcu_read_lock()
和rcu_read_unlock()
包圍。
4、RCU應(yīng)用場(chǎng)景
- 每種鎖都有自己適合場(chǎng)景:
spinlock
不分區(qū)reader
/writer
,對(duì)于些讀寫強(qiáng)度不對(duì)稱的是不適合的,RW spinlock
和seqlock
解決了這個(gè)問題,seqlock
傾向writer
,RW spinlock
傾向reader
。RCU適用于需要頻繁的讀取數(shù)據(jù),而相應(yīng)修改數(shù)據(jù)并不多的場(chǎng)景。比如:文件系統(tǒng)中,搜索定位目錄,而對(duì)目錄修改相對(duì)來(lái)講基本沒有。- RCU只能保護(hù)動(dòng)態(tài)分配的數(shù)據(jù)結(jié)構(gòu),并且必須是通過(guò)指針訪問該數(shù)據(jù)結(jié)構(gòu);
- 受RCU保護(hù)的臨界區(qū)不能
sleep
; - 讀寫不對(duì)稱,對(duì)
writer
的性能沒有特別的要求,但是reader
性能要求極高; -
reader
端對(duì)新舊數(shù)據(jù)不敏感。
二、內(nèi)存和優(yōu)化屏障
1、優(yōu)化屏障
-
在編程時(shí),指令一般不按照源程序順序執(zhí)行,原因是為提高程序執(zhí)行性能,會(huì)對(duì)它進(jìn)行優(yōu)化,主要為兩種:
- 編譯器優(yōu)化:提高系統(tǒng)的性能,編譯器在不影響邏輯的情況下會(huì)調(diào)整指令的順序。
- CPU執(zhí)行優(yōu)化:提高流水線的性能,CPU的亂序執(zhí)行可能會(huì)讓后面的沒有寄存器沖突和匯編指令先于前面的指令完成。
-
Linux使用
barrier()
函數(shù)實(shí)現(xiàn)優(yōu)化屏障,如其編譯器的優(yōu)化屏障源碼為:static inline void barrier(void) { /*asm表示插入?yún)R編語(yǔ)言程序;volatile表示阻止編譯對(duì)該值進(jìn)行優(yōu)化,確保變量使用了用 戶定義的精確地址,而不是裝有同意信息的一些別名。memory表示修改了內(nèi)存單元。*/ asm volatile("" : : : "memory"); }
-
優(yōu)化屏障的一個(gè)特定應(yīng)用是內(nèi)核搶占機(jī)制。要注意,
preempt_disable
對(duì)搶占計(jì)數(shù)器加1因而停用了搶占,preempt_enable
通過(guò)對(duì)搶占計(jì)數(shù)器減1而再次啟用搶占。這兩個(gè)命令之間的代碼,可免受搶占的影響??匆豢聪铝写a:preempt_disable(); function_which_must_not_be_preempted(); preempt_enable();
如果編譯器決定將代碼重新排序,那就會(huì)出現(xiàn)問題:
function_which_must_not_be_preempted(); preempt_disable(); preempt_enable();
另一種重排也會(huì)出現(xiàn)問題
preempt_disable(); preempt_enable(); function_which_must_not_be_preempted();
-
上述的錯(cuò)誤時(shí)間不會(huì)發(fā)生,因?yàn)?code>preempt_disable在搶占計(jì)數(shù)器加
1
之后插入一個(gè)內(nèi)存屏障,preempt_enable
在再次啟用搶占之前插入一個(gè)優(yōu)化屏障:
<preempt.h>#define preempt_disable() \ do { \ inc_preempt_count(); \ barrier(); \ } while (0)
這防止了編譯器將
inc_preempt_count()
與后續(xù)的語(yǔ)句交換位置。
<preempt.h>#define preempt_enable() \ do { \ ... barrier(); \ preempt_check_resched(); \ } while (0)
這種措施可以防止上文給出的第二種錯(cuò)誤的重排。
2、內(nèi)存屏障
-
內(nèi)存屏障是一種保證內(nèi)存訪問順序的方法,解決內(nèi)存訪問亂序問題:
- 編譯器編譯代碼時(shí)可能重新排序匯編指令,使編譯出來(lái)的程序在處理器上執(zhí)行速度更快,但是有的時(shí)候優(yōu)化結(jié)果可能不符合軟件開發(fā)工程師意圖。
- 新式處理器采用超標(biāo)量體系結(jié)構(gòu)和亂序執(zhí)行技術(shù),能夠在一個(gè)時(shí)鐘周期并行執(zhí)行多條指令。一句話總結(jié)為:順序取指令,亂序執(zhí)行,順序提交執(zhí)行結(jié)果。
- 多處理器系統(tǒng)當(dāng)中,硬件工程師使用存儲(chǔ)緩沖區(qū)等機(jī)制實(shí)現(xiàn)高效性能,引入處理器之間的內(nèi)存訪問亂序問題。|
-
處理器內(nèi)存屏障解決CPU之間的內(nèi)存訪問亂序問題和處理器訪問外圍設(shè)備的亂序問題。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-693723.html
內(nèi)存屏障類型 強(qiáng)制性的內(nèi)存屏障 SMP的內(nèi)存屏障 通用內(nèi)存屏障 mb ()
smp_mb ()
寫內(nèi)存屏障 wmb ()
smp_wmb ()
讀內(nèi)存屏障 rmb ()
smp_rmb ()
數(shù)據(jù)依賴屏障 read_barrier_depends()
smp_read_barrier_depends()
除數(shù)據(jù)依賴屏障之外,所有處理器內(nèi)存屏障隱含編譯器優(yōu)化屏障。注意:
smb_mb()
、smp_rmb()
、smp_wmb()
只在SMP系統(tǒng)中有硬件屏障,它們?cè)趩翁幚砥飨到y(tǒng)上產(chǎn)生的是軟件屏障(編譯器優(yōu)化屏障)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-693723.html
到了這里,關(guān)于Linux內(nèi)核源碼分析 (6)RCU機(jī)制及內(nèi)存優(yōu)化屏障的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!