ACCESS_ONCE的作用
Posted penghan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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(); }
以上是关于ACCESS_ONCE的作用的主要内容,如果未能解决你的问题,请参考以下文章