如果你看過 Linux 內核中的 RCU 的實現,你應該注意到了這個叫做 ACCESS_ONCE() 宏,但是並沒有很多人真正理解它的含義。網上有的地方甚至對此有錯誤的解釋,所以特寫此文來澄清一下。
雖然我早在讀 perfbook 之前就瞭解了 ACCESS_ONCE() 的含義(通過詢問大牛 Paul),但這本書中正好也沒有很詳細地介紹這個宏,所以就當是此書的讀書筆記了。
定義
它的定義很簡單,在 include/linux/compiler.h 的底部:
-
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
僅從語法上講,這似乎毫無意義,先取其地址,在通過指針取其值。而實際上不然,多了一個關鍵詞 volatile,所以它的含義就是強制編譯器每次使用 x 都從內存中獲取。
原因
僅僅從定義來看基本上看不大出來爲什麼要引入這麼一個東西。可以通過幾個例子(均來自 Paul,我做了小的修改)看一下。
1. 循環中有每次都要讀取的全局變量:
-
...
-
static int should_continue;
-
static void do_something(void);
-
...
-
while (should_continue)
-
do_something();
假設 do_something() 函數中並沒有對變量 should_continue 做任何修改,那麼,編譯器完全有可能把它優化成:
-
...
-
if (should_continue)
-
for (;;)
-
do_something();
這很好理解,不是嗎?對於單線程的程序,這麼做完全沒問題,可是對於多線程,問題就出來了:如果這個線程在執行do_something() 的期間,另外一個線程改變了 should_continue 的值,那麼上面的優化就是完全錯誤的了!更嚴重的問題是,編譯器根本就沒有辦法知道這段代碼是不是併發的,也就無從決定進行的優化是不是正確的!
這裏有兩種解決辦法:1) 給 should_continue 加鎖,畢竟多個進程訪問和修改全局變量需要鎖是很自然的;2) 禁止編譯器做此優化。加鎖的方法有些過了,畢竟 should_continue 只是一個布爾,而且退一步講,就算每次讀到的值不是最新的 should_continue 的值也可能是無所謂的,大不了多循環幾次,所以禁止編譯器做優化是一個更簡單也更容易的解決辦法。我們使用 ACCESS_ONCE() 來訪問 should_continue:
-
...
-
while (ACCESS_ONCE(should_continue))
-
do_something();
2. 指針讀取一次,但要dereference多次:
-
...
-
p = global_ptr;
-
if (p && p->s && p->s->func)
-
p->s->func();
那麼編譯器也有可能把它編譯成:
-
...
-
if (global_ptr && global_ptr->s && global_ptr->s->func)
-
global_ptr->s->func();
你可以譴責編譯器有些笨了,但事實上這是C標準允許的。這種情況下,另外的進程做了 global_ptr = NULL; 就會導致後一段代碼 segfault,而前一段代碼沒問題。同上,所以這時候也要用 ACCESS_ONCE():
-
...
-
p = ACCESS_ONCE(global_ptr);
-
if (p && p->s && p->s->func)
-
p->s->func();
3. watchdog 中的變量:
-
for (;;) {
-
still_working = 1;
-
do_something();
-
}
假設 do_something() 定義是可見的,而且沒有修改 still_working 的值,那麼,編譯器可能會把它優化成:
-
still_working = 1;
-
for (;;) {
-
do_something();
-
}
如果其它進程同時執行了:
-
for (;;) {
-
still_working = 0;
-
sleep(10);
-
if (!still_working)
-
panic();
-
}
通過 still_working 變量來檢測 wathcdog 是否停止了,並且等待10秒後,它確實停止了,panic()!經過編譯器優化後,就算它沒有停止也會 panic!!所以也應該加上 ACCESS_ONCE():
-
for (;;) {
-
ACCESS_ONCE(still_working) = 1;
-
do_something();
-
}
綜上,我們不難看出,需要使用 ACCESS_ONCE() 的兩個條件是:
1. 在無鎖的情況下訪問全局變量;
2. 對該變量的訪問可能被編譯器優化成合併成一次(上面第1、3個例子)或者拆分成多次(上面第2個例子)。
例子
Linus 在郵件中給出的另外一個例子是:
編譯器有可能把下面的代碼:
-
if (a> MEMORY) {
-
do1;
-
do2;
-
do3;
-
} else {
-
do2;
-
}
優化成:
-
if (a> MEMORY)
-
do1;
-
do2;
-
if (a> MEMORY)
-
do3;
這裏完全符合上面我總結出來的兩個條件,所以也應該使用 ACCESS_ONCE()。正如 Linus 所說,不是編譯器一定會這麼優化,而是你無法證明它不會做這樣的優化。
So the rule is: if you access unlocked values, you use ACCESS_ONCE(). You
don't say "but it can't matter". Because you simply don't know.
再看實際中的例子:
commit 0ad92ad03aa444b312bd318b0341011a8be09d13 Author: Eric Dumazet Date: Tue Nov 1 12:56:59 2011 +0000 udp: fix a race in encap_rcv handling udp_queue_rcv_skb() has a possible race in encap_rcv handling, since this pointer can be changed anytime. We should use ACCESS_ONCE() to close the race. diff --git a/net/ipv4/udp.c b/net/ipv4/udp.c index 131d8a7..ab0966d 100644 --- a/net/ipv4/udp.c +++ b/net/ipv4/udp.c @@ -1397,6 +1397,8 @@ int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) nf_reset(skb); if (up->encap_type) { + int (*encap_rcv)(struct sock *sk, struct sk_buff *skb); + /* * This is an encapsulation socket so pass the skb to * the socket's udp_encap_rcv() hook. Otherwise, just @@ -1409,11 +1411,11 @@ int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) */ /* if we're overly short, let UDP handle it */ - if (skb->len > sizeof(struct udphdr) && - up->encap_rcv != NULL) { + encap_rcv = ACCESS_ONCE(up->encap_rcv); + if (skb->len > sizeof(struct udphdr) && encap_rcv != NULL) { int ret; - ret = (*up->encap_rcv)(sk, skb); + ret = encap_rcv(sk, skb); if (ret <= 0) { UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INDATAGRAMS,
更多
或許看了上面的會讓你有一種錯覺,volatile 可以解決同步的問題,其實不然,它只解決其中一個方面。而且上面所有的例子有一個共同的特點:所有的寫操作都是簡單的賦值(相對於大於CPU字寬的結構體賦值),簡單賦值操作在所有平臺上都是原子性的,而如果是做加法操作,原子性未必可以保證,更不用說需要 memory barrier 的時候了。所以,不要濫用 volatile。