ACCESS_ONCE的作用

如果你看过 Linux 内核中的 RCU 的实现,你应该注意到了这个叫做 ACCESS_ONCE() 宏。

ACCESS_ONCE的定义如下:

#define __ACCESS_ONCE(x) ({ \
     __maybe_unused typeof(x) __var = (__force typeof(x)) 0; \
    (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 = 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();
}