为什么不应该使用“volatile”类型类

C 程序员经常认为 volatile 意味着变量可以在当前执行线程之外被更改;因此,当使用共享数据结构时,他们有时会试图在内核代码中使用它。换句话说,他们已知将 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。在处理共享数据时,正确的锁定使得 volatile 不必要,并且可能有害。

volatile 存储类最初用于内存映射的 I/O 寄存器。在内核中,寄存器访问也应受到锁的保护,但是人们也不希望编译器在临界区内“优化”寄存器访问。但是,在内核中,I/O 内存访问始终通过访问器函数完成;不赞成通过指针直接访问 I/O 内存,并且在所有体系结构上都无法正常工作。这些访问器被编写为防止不必要的优化,因此,再次强调,volatile 是不必要的。

可能诱使人们使用 volatile 的另一种情况是,处理器正在忙等待某个变量的值。执行忙等待的正确方法是

while (my_variable != what_i_want)
    cpu_relax();

cpu_relax() 调用可以降低 CPU 功耗或让位于超线程双处理器;它也碰巧充当编译器屏障,因此,再次强调,volatile 是不必要的。当然,忙等待通常从一开始就是一种反社会行为。

在内核中,仍然存在一些罕见的情况,volatile 是有意义的

  • 上述访问器函数可能会在直接 I/O 内存访问有效的体系结构上使用 volatile。本质上,每个访问器调用都会变成一个小的临界区,并确保访问按照程序员的预期发生。

  • 更改内存但没有其他可见副作用的内联汇编代码有被 GCC 删除的风险。将 volatile 关键字添加到 asm 语句将防止此删除。

  • jiffies 变量是特殊的,因为每次引用它时,它都可以具有不同的值,但是可以读取它而无需任何特殊锁定。因此,jiffies 可以是 volatile 的,但是强烈反对添加此类型的其他变量。在这方面,jiffies 被认为是“愚蠢的遗留”问题(Linus 的话);修复它会比它本身的价值带来更多麻烦。

  • 指向相干内存中数据结构的指针(可能被 I/O 设备修改)有时可以合理地设为 volatile。网络适配器使用的环形缓冲区,其中该适配器更改指针以指示已处理的描述符,是这种情况的一个示例。

对于大多数代码,上述对 volatile 的辩解均不适用。因此,使用 volatile 很可能会被视为错误,并会给代码带来额外的审查。试图使用 volatile 的开发人员应退后一步,思考他们真正想要完成的事情。

删除 volatile 变量的补丁通常是受欢迎的 - 只要它们附带一个证明并发问题已得到适当考虑的理由。

参考资料

[1] https://lwn.net/Articles/233481/

[2] https://lwn.net/Articles/233482/

鸣谢

Randy Dunlap 的原始推动和研究

由 Jonathan Corbet 撰写

通过 Satyam Sharma、Johannes Stezenbach、Jesper Juhl、Heikki Orsila、H. Peter Anvin、Philipp Hahn 和 Stefan Richter 的评论进行了改进。