C程序员经常用volatile来表示可以在当前执行线程之外更改变量。结果,当使用共享数据结构时,他们有时会倾向于在内核代码中使用它。换句话说,已知它们将易失性类型视为一种简单的原子变量,而事实并非如此。在内核代码中使用volatile几乎是不正确的。本文档描述了原因。
理解volatile的关键点在于其目的是抑制优化,这几乎从来不是人们真正想要做的。在内核中,必须保护共享数据结构免受不必要的并发访问,这是非常不同的任务。防止不必要的并发的过程还将以更有效的方式避免几乎所有与优化相关的问题。
与volatile一样,使并发访问数据安全(自旋锁,互斥锁,内存屏障等)的内核原语旨在防止不必要的优化。如果正确使用它们,那么也将无需使用volatile。如果仍然需要使用volatile,那么几乎可以肯定某个地方的代码中存在一个错误。在正确编写的内核代码中,volatile仅可减慢速度。
考虑一个典型的内核代码块:
spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);
如果所有代码都遵循锁定规则,则在保持the_lock的情况下shared_data的值不会意外更改。其他任何可能要与该数据一起使用的代码都将在锁上等待。自旋锁原语充当内存屏障-它们被明确编写为这样做-意味着不会在它们之间优化数据访问。因此,编译器可能认为自己知道了shared_data中的内容,但是spin_lock()调用由于充当内存屏障,将迫使其忘记所有已知信息。访问该数据不会有优化问题。
如果shared_data声明为volatile,则仍然需要锁定。但是,编译器也将被从优化访问shared_data防止中的关键部分,当我们知道没有其他人可以用它来工作。持有锁定时,shared_data不会不稳定。在处理共享数据时,适当的锁定使volatile变得不必要-并可能有害。
易失性存储类最初用于内存映射的I / O寄存器。在内核中,寄存器访问也应该受到锁的保护,但是也不希望编译器在关键部分内“优化”寄存器访问。但是,在内核中,I / O内存访问总是通过访问器功能完成的。直接通过指针访问I / O内存的方法被皱了皱眉,并且不适用于所有体系结构。编写这些访问器是为了防止不必要的优化,因此,再次不需要volatile。
可能会尝试使用volatile的另一种情况是,当处理器忙于等待变量的值时。执行繁忙等待的正确方法是:
while (my_variable != what_i_want)
cpu_relax();
调用cpu_relax()可以降低CPU功耗或降低超线程双处理器的功耗。它也恰好是一个内存屏障,因此,再一次不需要挥发。当然,开始忙碌通常是一种反社会行为。
在内核中,仍然有一些罕见的情况使volatile有意义:
在直接I / O内存访问确实起作用的体系结构上,上述访问器功能可能会使用volatile。从本质上讲,每个访问器调用本身都会变成一个关键部分,并确保按程序员的预期进行访问。
内联汇编代码会更改内存,但没有其他可见的副作用,可能会被GCC删除。将volatile关键字添加到asm语句中将防止此删除。
jiffies变量的特殊之处在于,每次引用时它都可以具有不同的值,但是无需任何特殊锁定即可读取它。因此jiffies可能是易变的,但是强烈反对添加此类其他变量。在这方面,吉菲斯被认为是“愚蠢的遗产”问题(利纳斯的话);修复它比它值得的麻烦更多。
指向一致性存储器中可能由I / O设备修改的数据结构的指针有时可能会易失。网络适配器使用的环形缓冲区就是这种情况的一个示例,其中该适配器更改指针以指示已处理了哪些描述符。
对于大多数代码,以上所有关于volatile的理由均不适用。结果,使用volatile可能会被视为一个错误,并将对代码进行更多审查。试图使用volatile的开发人员应该退后一步,思考他们真正想要实现的目标。