ACCESS_ONCE(x)宏含義

如果你看過 Linux 內核中的 RCU 的實現,你應該注意到了這個叫做 ACCESS_ONCE() 宏,但是並沒有很多人真正理解它的含義。網上有的地方甚至對此有錯誤的解釋,所以特寫此文來澄清一下。

雖然我早在讀 perfbook 之前就瞭解了 ACCESS_ONCE() 的含義(通過詢問大牛 Paul),但這本書中正好也沒有很詳細地介紹這個宏,所以就當是此書的讀書筆記了。

定義

它的定義很簡單,在 include/linux/compiler.h 的底部:

C:
  1. #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

僅從語法上講,這似乎毫無意義,先取其地址,在通過指針取其值。而實際上不然,多了一個關鍵詞 volatile,所以它的含義就是強制編譯器每次使用 x 都從內存中獲取。

原因
僅僅從定義來看基本上看不大出來爲什麼要引入這麼一個東西。可以通過幾個例子(均來自 Paul,我做了小的修改)看一下。

1. 循環中有每次都要讀取的全局變量:

C:
  1. ...
  2. static int should_continue;
  3. static void do_something(void);
  4. ...
  5.                while (should_continue)
  6.                        do_something();

假設 do_something() 函數中並沒有對變量 should_continue 做任何修改,那麼,編譯器完全有可能把它優化成:

C:
  1. ...
  2.                if (should_continue)
  3.                        for (;;)
  4.                                do_something();

這很好理解,不是嗎?對於單線程的程序,這麼做完全沒問題,可是對於多線程,問題就出來了:如果這個線程在執行do_something() 的期間,另外一個線程改變了 should_continue 的值,那麼上面的優化就是完全錯誤的了!更嚴重的問題是,編譯器根本就沒有辦法知道這段代碼是不是併發的,也就無從決定進行的優化是不是正確的!

這裏有兩種解決辦法:1) 給 should_continue 加鎖,畢竟多個進程訪問和修改全局變量需要鎖是很自然的;2) 禁止編譯器做此優化。加鎖的方法有些過了,畢竟 should_continue 只是一個布爾,而且退一步講,就算每次讀到的值不是最新的 should_continue 的值也可能是無所謂的,大不了多循環幾次,所以禁止編譯器做優化是一個更簡單也更容易的解決辦法。我們使用 ACCESS_ONCE() 來訪問 should_continue:

C:
  1. ...
  2.      while (ACCESS_ONCE(should_continue))
  3.                        do_something();

2. 指針讀取一次,但要dereference多次:

C:
  1. ...
  2.     p = global_ptr;
  3.     if (&& p->s && p->s->func)
  4.         p->s->func();

那麼編譯器也有可能把它編譯成:

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():

C:
  1. ...
  2.     p = ACCESS_ONCE(global_ptr);
  3.     if (&& p->s && p->s->func)
  4.         p->s->func();

3. watchdog 中的變量:

C:
  1. for (;;) {
  2.                        still_working = 1;
  3.                        do_something();
  4.                }

假設 do_something() 定義是可見的,而且沒有修改 still_working 的值,那麼,編譯器可能會把它優化成:

C:
  1. still_working = 1;
  2.                for (;;) {
  3.                        do_something();
  4.                }

如果其它進程同時執行了:

C:
  1. for (;;) {
  2.                        still_working = 0;
  3.                        sleep(10);
  4.                        if (!still_working)
  5.                                panic();
  6.                }

通過 still_working 變量來檢測 wathcdog 是否停止了,並且等待10秒後,它確實停止了,panic()!經過編譯器優化後,就算它沒有停止也會 panic!!所以也應該加上 ACCESS_ONCE():

C:
  1. for (;;) {
  2.                        ACCESS_ONCE(still_working) = 1;
  3.                        do_something();
  4.                }

綜上,我們不難看出,需要使用 ACCESS_ONCE() 的兩個條件是:

1. 在無鎖的情況下訪問全局變量;
2. 對該變量的訪問可能被編譯器優化成合併成一次(上面第1、3個例子)或者拆分成多次(上面第2個例子)。

例子

Linus 在郵件中給出的另外一個例子是:

編譯器有可能把下面的代碼:

C:
  1. if (a> MEMORY) {
  2.         do1;
  3.         do2;
  4.         do3;
  5.     } else {
  6.         do2;
  7.     }

優化成:

C:
  1. if (a> MEMORY)
  2.         do1;
  3.     do2;
  4.     if (a> MEMORY)
  5.         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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章