rcu_assign_pointer、rcu_dereference、ACCESS_ONCE
題記:看代碼不要死死的一條一條往后看,要適當?shù)穆?lián)系上下文,看出整體邏輯性,流程性。事實上,編譯器和處理器也不是呆板的一條一條往后執(zhí)行的,它也會有預取和邏輯判斷。
?
由內(nèi)存屏障到RCU的發(fā)布訂閱
內(nèi)存屏障主要解決的問題是編譯器的優(yōu)化和CPU的亂序執(zhí)行。
?
編譯器在優(yōu)化的時候,生成的匯編指令可能和c語言程序的執(zhí)行順序不一樣,在需要程序嚴格按照c語言順序執(zhí)行時,需要顯式的告訴編譯不需要優(yōu)化,這在linux下是通過barrier()宏完成的,它依靠volidate關(guān)鍵字和memory關(guān)鍵字,前者告訴編譯barrier()周圍的指令不要被優(yōu)化,后者作用是告訴編譯器匯編代碼會使內(nèi)存里面的值更改,編譯器應使用內(nèi)存里的新值而非寄存器里保存的老值。
?
同樣,CPU執(zhí)行會通過亂序以提高性能。匯編里的指令不一定是按照我們看到的順序執(zhí)行的。linux中通過mb()系列宏來保證執(zhí)行的順序。簡單的說,如果在程序某處插入了mb()/rmb()/wmb()宏,則宏之前的程序保證比宏之后的程序先執(zhí)行,從而實現(xiàn)串行化。
?
即使是編譯器生成的匯編碼有序,處理器也不一定能保證有序。就算編譯器生成了有序的匯編碼,到了處理器那里也拿不準是不是會按照代碼順序執(zhí)行。所以就算編譯器保證有序了,程序員也還是要往代碼里面加內(nèi)存屏障才能保證絕對訪存有序,這倒不如編譯器干脆不管算了,因為內(nèi)存屏障本身就是一個sequence point,加入后已經(jīng)能夠保證編譯器也有序。
?
處理器雖然亂序執(zhí)行,但最終會得出正確的結(jié)果,所以邏輯上講程序員本不需要關(guān)心處理器亂序的問題。但是在SMP并發(fā)執(zhí)行的情況下,處理器無法知道并發(fā)程序之間的邏輯,比如,在不同core上的讀者和寫者之間的邏輯。簡單講,處理器只保證在單個core上按照code中的順序給出最終結(jié)果。這就要求程序員通過mb()/rmb()/wmb()/read_barrier_depends來告知處理器,從而得到正確的并發(fā)結(jié)果。內(nèi)存屏障、數(shù)據(jù)依賴屏障都是為了處理SMP環(huán)境下的數(shù)據(jù)同步問題,UP根本不存在這個問題。
?
下面分析下內(nèi)存屏障在RCU上的應用:
#define rcu_assign_pointer(p,v)??????? ({ \
???????????????????????????????????????????????????????????????????????smp_wmb();\
???????????????????????????????????????????????????????????????????????(p)= (v); \
???????????????????????????????????????????????})
?
#define rcu_dereference(p)???? ({ \
???????????????????????????????????????????????typeof(p)_________p1 =ACCESS_ONCE(p); \
???????????????????????????????????????????????smp_read_barrier_depends();\
???????????????????????????????????????????????(_________p1);\
???????????????????????????????????????????????})?
? ? ? ??
rcu_assign_pointer()通常用于寫者的發(fā)布,rcu_dereference()通常用于讀者的訂閱。
?
寫者:
1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);
?
讀者:
1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();
rcu_assign_pointer()是說,先把那塊內(nèi)存寫好,再把指針指過去。這里使用的內(nèi)存寫屏障是為了保證并發(fā)的讀者讀到數(shù)據(jù)一致性。在這條語句之前的讀者讀到舊的指針和舊的內(nèi)存,這條語句之后的讀者讀到新的指針和新的內(nèi)存。如果沒有這條語句,很有可能出現(xiàn)讀者讀到新的指針和舊的內(nèi)存。也就是說,這里通過內(nèi)存屏障刷新了p所指向的內(nèi)存的值,至于gp本身的值有沒有更新還不確定。實際上,gp本身值的真正更新要等到并發(fā)的讀者來促發(fā)。
rcu_dereference()原語用的是數(shù)據(jù)依賴屏障,smp_read_barrier_dependence,它要求后面的讀操作如果依賴前面的讀操作,則前面的讀操作需要首先完成。根據(jù)數(shù)據(jù)之間的依賴,要讀p->a, p->b, p->c,就必須先讀p,要先讀p,就必須先讀p1,要先讀p1,就必須先讀gp。也就是說讀者所在的core在進行后續(xù)的操作之前,gp必須是同步過的當前時刻的最新值。如果沒有這個數(shù)據(jù)依賴屏障,有可能讀者所在的core很長一段時間內(nèi)一直用的是舊的gp值。所以,這里使用數(shù)據(jù)依賴屏障是為了督促寫者將gp值準備好,是為了呼應寫者,這個呼應的訴求是通過數(shù)據(jù)之間的依賴關(guān)系來促發(fā)的,也就是說到了非呼應不可的地步了。
下面看看kernel中常用的鏈表操作是如何使用這樣的發(fā)布、訂閱機制的:
寫者:
static inline void list_add_rcu(struct list_head *new,struct list_head *head)
{
__list_add_rcu(new, head, head->next);
}
static inline void __list_add_rcu(struct list_head * new,
struct list_head * prev, struct list_head * next)
{
new->next = next;
new->prev = prev;
smp_wmb();
next->prev = new;
prev->next = new;
}
?
讀者:
#define list_for_each_entry_rcu(pos, head, member) \
???????????for(pos = list_entry((head)->next, typeof(*pos), member); \
???????????????????????prefetch(rcu_dereference(pos)->member.next),\
???????????????????????????????????&pos->member!= (head); \
???????????????????????pos= list_entry(pos->member.next, typeof(*pos), member))
?
寫者通過調(diào)用list_add_rcu來發(fā)布新的節(jié)點,其實是發(fā)布next->prev, prev->next這兩個指針。讀者通過list_for_each_entry_rcu來訂閱這連個指針,我們將list_for_each_entry_rcu訂閱部分簡化如下:
pos = prev->next;
prefetch(rcu_dereference(pos)->next);
?
讀者通過rcu_dereference訂閱的是pos,而由于數(shù)據(jù)依賴關(guān)系,又間接訂閱了prev->next指針,或者說是促發(fā)prev->next的更新。
?
由RCU引出的ACCESS_ONCE宏
定義
它的定義很簡單,在?include/linux/compiler.h的底部:
PLAIN TEXT
C:
1.??#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
僅從語法上講,這似乎毫無意義,先取其地址,在通過指針取其值。而實際上不然,多了一個關(guān)鍵詞?volatile,所以它的含義就是強制編譯器每次使用?x都從內(nèi)存中獲取。
原因
僅僅從定義來看基本上看不大出來為什么要引入這么一個東西。可以通過幾個例子(均來自Paul,我做了小的修改)看一下。
1.?循環(huán)中有每次都要讀取的全局變量:
PLAIN TEXT
C:
1.??...
2.??static?int?should_continue;
3.??static?void?do_something(void);
4.??...
5.??? ? ? ? ? ? ? ?while?(should_continue)
6.??? ? ? ? ? ? ? ?? ? ? ?do_something();
假設(shè)?do_something()函數(shù)中并沒有對變量?should_continue做任何修改,那么,編譯器完全有可能把它優(yōu)化成:
PLAIN TEXT
C:
1.??...
2.??? ? ? ? ? ? ? ?if?(should_continue)
3.??? ? ? ? ? ? ? ?? ? ? ?for?(;;)
4.??? ? ? ? ? ? ? ?? ? ? ? ? ? ? ?do_something();
這很好理解,不是嗎?對于單線程的程序,這么做完全沒問題,可是對于多線程,問題就出來了:如果這個線程在執(zhí)行do_something()的期間,另外一個線程改變了?should_continue的值,那么上面的優(yōu)化就是完全錯誤的了!更嚴重的問題是,編譯器根本就沒有辦法知道這段代碼是不是并發(fā)的,也就無從決定進行的優(yōu)化是不是正確的!
這里有兩種解決辦法:1)給?should_continue加鎖,畢竟多個進程訪問和修改全局變量需要鎖是很自然的;2)禁止編譯器做此優(yōu)化。加鎖的方法有些過了,畢竟?should_continue只是一個布爾,而且退一步講,就算每次讀到的值不是最新的?should_continue的值也可能是無所謂的,大不了多循環(huán)幾次,所以禁止編譯器做優(yōu)化是一個更簡單也更容易的解決辦法。我們使用?ACCESS_ONCE()來訪問?should_continue:
PLAIN TEXT
C:
1.??...
2.??? ? ?while?(ACCESS_ONCE(should_continue))
3.??? ? ? ? ? ? ? ?? ? ? ?do_something();
2.?指針讀取一次,但要dereference多次:
PLAIN TEXT
C:
1.??...
2.??? ??p?=?global_ptr;
3.??? ??if?(p?&&?p->s?&&?p->s->func)
4.??? ? ? ? p->s->func();
那么編譯器也有可能把它編譯成:
PLAIN TEXT
C:
1.??...
2.??? ??if?(global_ptr?&&?global_ptr->s?&&?global_ptr->s->func)
3.??? ? ? ? global_ptr->s->func();
你可以譴責編譯器有些笨了,但事實上這是C標準允許的。這種情況下,另外的進程做了?global_ptr = NULL;就會導致后一段代碼?segfault,而前一段代碼沒問題。同上,所以這時候也要用?ACCESS_ONCE():
PLAIN TEXT
C:
1.??...
2.??? ??p?=?ACCESS_ONCE(global_ptr);
3.??? ??if?(p?&&?p->s?&&?p->s->func)
4.??? ? ? ? p->s->func();
3. watchdog中的變量:
PLAIN TEXT
C:
1.??for?(;;)?{
2.??? ? ? ? ? ? ? ?? ? ? ?still_working?=?1;
3.??? ? ? ? ? ? ? ?? ? ? ?do_something();
4.??? ? ? ? ? ? ? ?}
假設(shè)?do_something()定義是可見的,而且沒有修改?still_working的值,那么,編譯器可能會把它優(yōu)化成:
PLAIN TEXT
C:
1.??still_working?=?1;
2.??? ? ? ? ? ? ? ?for?(;;)?{
3.??? ? ? ? ? ? ? ?? ? ? ?do_something();
4.??? ? ? ? ? ? ? ?}
如果其它進程同時執(zhí)行了:
PLAIN TEXT
C:
1.??for?(;;)?{
2.??? ? ? ? ? ? ? ?? ? ? ?still_working?=?0;
3.??? ? ? ? ? ? ? ?? ? ? ?sleep(10);
4.??? ? ? ? ? ? ? ?? ? ? ?if?(!still_working)
5.??? ? ? ? ? ? ? ?? ? ? ? ? ? ? ?panic();
6.??? ? ? ? ? ? ? ?}
通過?still_working變量來檢測?wathcdog是否停止了,并且等待10秒后,它確實停止了,panic()!經(jīng)過編譯器優(yōu)化后,就算它沒有停止也會?panic!!所以也應該加上?ACCESS_ONCE():
PLAIN TEXT
C:
1.??for?(;;)?{
2.??? ? ? ? ? ? ? ?? ? ? ?ACCESS_ONCE(still_working)?=?1;
3.??? ? ? ? ? ? ? ?? ? ? ?do_something();
4.??? ? ? ? ? ? ? ?}
綜上,我們不難看出,需要使用?ACCESS_ONCE()的兩個條件是:
1.?在無鎖的情況下訪問全局變量;
2.?對該變量的訪問可能被編譯器優(yōu)化成合并成一次(上面第1、3個例子)或者拆分成多次(上面第2個例子)。
例子
Linus?在郵件中給出的另外一個例子是:
編譯器有可能把下面的代碼:
PLAIN TEXT
C:
1.??if?(a> MEMORY)?{
2.??? ? ? ? do1;
3.??? ? ? ? do2;
4.??? ? ? ? do3;
5.??? ??}?else?{
6.??? ? ? ? do2;
7.??? ??}
優(yōu)化成:
PLAIN TEXT
C:
1.??if?(a> MEMORY)
2.??? ? ? ? do1;
3.??? ? do2;
4.??? ??if?(a> MEMORY)
5.??? ? ? ? do3;
這里完全符合上面我總結(jié)出來的兩個條件,所以也應該使用?ACCESS_ONCE()。正如?Linus?所說,不是編譯器一定會這么優(yōu)化,而是你無法證明它不會做這樣的優(yōu)化。
總結(jié)
以上是生活随笔為你收集整理的rcu_assign_pointer、rcu_dereference、ACCESS_ONCE的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AB1601的OTA区224K存储空间的
- 下一篇: AB1601安装新版本IDE后工程编译可