# Day 8 Why the “volatile” type class should not be used

今天想要来记录这篇文件 Why the “volatile” type class should not be used,契机是用 ${linux}/scripts 做 checkpatch.pl 的时候,只要有用到 volatile 这个修饰词,一定会吐出如下的讯息:Use of volatile is usually wrong: see Documentation/volatile-considered-harmful.txt

我们接着看下去~

文件

:Original: :ref:`Documentation/process/volatile-considered-harmful.rst
           <volatile_considered_harmful>`

爲什麽不应该使用「volatile」类型
================================

C 语言程序设计师通常认爲 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 的情况是,当处理器正 busy-waiting 一个变数。
正确执行一个 busy-waiting 的方法是::

    while (my_variable != what_i_want)
        cpu_relax();

cpu_relax() 会降低 CPU 的能量消耗或者转交给超执行绪的另一个执行绪;它像是记忆体屏障一样,
所以,再次,volatile 不是必需的。当然,busy-waiting 一开始就是一种反常规的做法。

核心中,在很少数的情况下 volatile 仍然是有意义的:

  - 在一些架构上,允许直接的 I/O 记忆体存取,那麽前面提到的存取函数可以使用 volatile。
    基本上,每一个存取函数呼叫,它自己都是一个小的临界区域并且保证按程序设计师的期望做存取。

  - 某些会改变记忆体状态的内嵌组合语言,虽然没有其他明显的作用,但是有被 GCC 优化掉的可能。
    在内嵌组语中加上 volatile 关键字可以防止这种优化。

  - Jiffies 变数是一种特殊情况,在於每次读取它的时候都可以有不同的值,
    但读取 jiffies 变数时不需要任何特殊的加锁保护。
    所以 jiffies 变数可以使用 volatile,但是不赞成其他跟 jiffies 相同类型变数使用 volatile。
    Jiffies被认爲是一种"愚蠢的遗留物"(Linus 说的),因爲解决这个问题比保持现状要麻烦的多。

  - 由於某些 I/O 设备可能会修改一致性记忆体,
    所以有时指向连续一致性记忆体资料结构的指标需要正确的使用 volatile。
    网路卡使用的 ring buffer 是这类情形的一个例子,
    其中网路卡用改变指标来表示哪些描述子已经处理过了。

对於大多程序码,上述几种可以使用 volatile 的情况都不适用。
所以,使用 volatile 是一种 bug 并且需要对这样的程序码额外仔细检查。
那些试图使用 volatile 的开发人员需要好好思考他们真正想实现的是什麽。

非常欢迎删除 volatile 变量的 patch - 只要证明这些 patch 完整的考虑了并行问题。

参考文件
-------

[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 的意见改善了本文件。

我的理解

  • 结论就是不确定自己在做什麽就不要用 volatile XD
  • 上述提到较合理使用 volatile 的实际使用情境如下:(不过 ring buffer 的还没有找到)
    • accessor function、inline asm
      • 在参考文件中,Linus 有提到 "并不是变数是 volatile,而是要存取的这个动作是 volatile",所以这里可以看见,函数的参数都有被冠上 volatile 的修饰词。
    • jiffies

同场加映

在 checkpatch 的时候,还有遇到其他几种不同的 error/warning,也一并记录起来:

後记

今天作为,在一个系列结束和下一个系列开始的,缓冲日,
明天就会正式进入 Cache and TLB Flushing Under Linux 的系列罗!
我们明天见!


<<:  IT铁人DAY 2-物件导向基本概念(1)

>>:  [Day14] Android - Kotlin笔记:LiveData在fragment重建时会重新呼叫两次的解决方法

Day 12:vim 配色方案

俗话说人要衣装,佛要金装,我们的 vim 也得要有漂亮的外观。今天就让我们来看看如何调教调整 vim...

[Android 错误处理大全] 解决在 Debug 版进行 Facebook 登入失败

近期在接 Facebook SDK 做第三方登入时发现 只要不是 Release 版的 apk 就无...

DAY07 - [CSS+RWD] 导览列

今日文章目录 > - 导览列 > - 练习演示 > - 遇到的问题 > -...

Powershell 入门参数属性(2)

前面我们讲了怎么通过参数的属性限制参数的个数和长度。今天我们来看看,通过参数的属性,限制参数的格式以...

裸机Hyperviser之间比较

但市面上的裸机Hyperviser还有其他选择(ESXI, Proxmox VE…),为何独锺unR...