如果你看过 Linux 内核中的 RCU 的实现,你应该注意到了这个叫做 ACCESS_ONCE() 宏。
ACCESS_ONCE的定义如下:
#define __ACCESS_ONCE(x) ({ \
__maybe_unused typeof(x) __var = (__force typeof(x)) ; \
(volatile typeof(x) *)&(x); })
#define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
仅从语法上讲,这似乎毫无意义,先取其地址,在通过指针取其值。而实际上不然,多了一个关键词 volatile,所以它的含义就是强制编译器每次使用 x 都从内存中获取。
原因:
可以通过几个例子看一下。
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 = ;
do_something();
}
假设 do_something() 定义是可见的,而且没有修改 still_working 的值,那么,编译器可能会把它优化成:
still_working = ;
for (;;) {
do_something();
}
如果其它进程同时执行了:
for (;;) {
still_working = ;
sleep();
if (!still_working)
panic();
}
通过 still_working 变量来检测 wathcdog 是否停止了,并且等待10秒后,它确实停止了,panic()!经过编译器优化后,就算它没有停止也会 panic!!所以也应该加上 ACCESS_ONCE():
for (;;) {
ACCESS_ONCE(still_working) = ;
do_something();
}