为什么不应该使用"volatile"类型的类
C程序员通常认为volatile意味着变量可以在当前执行线程之外被改变;因此,当使用共享数据结构时,他们有时会倾向于在内核代码中使用它。换句话说,他们已经将volatile类型视为一种简单的原子变量,但实际上并非如此。在内核代码中几乎从不正确地使用volatile;本文描述了其中的原因。
理解volatile的关键点是,它的目的是抑制优化,而这几乎从不是我们真正想要做的。在内核中,我们必须保护共享数据结构免受不希望的并发访问,这是完全不同的任务。保护不希望的并发性的过程也会以更高效的方式避免几乎所有与优化相关的问题。
与volatile类似,使并发访问数据安全的内核原语(自旋锁、互斥锁、内存屏障等)旨在防止不希望的优化。如果它们被正确使用,就不需要再使用volatile。如果仍然需要volatile,那么代码中几乎肯定存在bug。在编写正确的内核代码中,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变量的补丁 - 只要它们附带了合理的理由,表明并发问题已经经过适当的思考。