为什么不应该使用“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 删除的风险。在 `asm` 语句中添加 volatile 关键字将防止这种删除。

  • `jiffies` 变量的特殊之处在于它每次被引用时都可以有不同的值,但无需任何特殊锁定即可读取。因此 `jiffies` 可以是 volatile,但强烈不鼓励添加此类型的其他变量。在此方面,`jiffies` 被认为是“愚蠢的遗留问题”(Linus 的原话);修复它将得不偿失。

  • 指向可能由 I/O 设备修改的连贯内存中数据结构的指针,有时可以合理地是 volatile。网络适配器使用的环形缓冲区就是一个例子,其中适配器更改指针以指示哪些描述符已处理。

对于大多数代码,上述使用 volatile 的理由都不适用。因此,使用 volatile 很可能被视为一个 bug,并将导致代码受到额外的审查。倾向于使用 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。