跳到主要内容

4.1 VOLATILE关键字

当你使用一个不能被编译器优化的变量时,需要使用volatile关键字,如果在需要使用 volatile 的地方没有使用它,编译器可能会优化对变量的访问,从而生成非预期的代码,甚至移除程序中原本预期的功能。

4.1.1 volatile 的含义​

将变量声明为 volatile,告诉编译器该变量可能在任何时间被当前实现代码之外的代码修改。例如:

  • 由操作系统修改
  • 由硬件修改

这种声明保证编译器不会基于“变量未被使用或未被修改”的假设对其进行优化。你也可以使用 volatile 来告诉编译器:某个包含内联汇编(inline assembly)的代码块具有输入列表、输出列表和 clobber 列表所不能完全描述的副作用。

4.1.2 何时使用 volatile​

当一个变量可能会在其定义作用域之外被修改时,应当使用 volatile 关键字。例如,如果程序在某次计算中使用了一个全局变量,编译器通常会生成指令,将该变量的值加载到寄存器中以执行计算。如果该全局变量随后在另一段计算中再次使用,编译器可能会复用寄存器中已有的值,而不是重新从内存加载。这种复用行为的原因在于:

编译器优化器默认假设非 volatile 变量不会被外部实体修改。但这个假设对于内存映射外设(memory-mapped peripherals)是不成立的 ——外设寄存器的值经常会在程序未知的情况下被硬件修改。具体请参考 4.1.5 “未使用 volatile 关键字导致的无限循环示例”。

另一个例子是:某个变量可能被用来实现 sleep 或定时延迟。如果该变量看起来未被使用,编译器可能会移除这段用于计时延迟的代码,除非该变量被声明为 volatile。

在实践中:

  • 当访问内存映射外设时,必须将变量声明为 volatile。即使在 -O0 优化等级下,也不能保证编译器会默认把所有相关变量视为 volatile。
  • 中断处理程序中会修改的变量并在外部使用时,应使用 volatile关键字修饰

4.1.3 未使用 volatile 时可能出现的潜在问题​

当一个需要声明为 volatile 的变量没有声明为 volatile,编译器会假定该变量的值不会在其定义作用域之外被修改。因此,编译器可能会执行一些不希望发生的优化。这个问题可能以多种形式表现出来,例如:

  • 在轮询硬件时,代码可能会卡在循环中
  • 优化可能导致实现刻意延迟(timing delay)的代码被移除

4.1.4 强制使用特定指令访问内存​

将变量声明为volatile并不能保证编译器在访问该变量时使用某条特定的机器指令。仅仅在变量或指针类型上添加 volatile 修饰符,并不能保证编译器会自动选择该寄存器所要求的访问方式。如果你的代码需要特定访问方式的内存映射位置,那么仅声明为 volatile 是不够的。你还必须使用 __asm__ 内联汇编语句,并在其中显式写入你所需要的加载或存储指令。

4.1.5 未使用 volatile 关键字导致的无限循环示例​

下表中的两个示例对比了buffer_full不使用volatile与使用volatile两种写法。

表4.1 buffer 循环的非 volatile和volatile 版本C语言代码

buffer 循环的非 volatile 版本buffer 循环的 volatile 版本
buffer 循环的非 volatile 版本buffer 循环的 volatile 版本

两个例程都在循环中递增计数器,直到buffer_full被置为true。buffer_full可能在程序流程之外被异步修改。

左例未将buffer_full声明为volatile,编译器只在首次访问时从内存加载该变量,之后复用寄存器中的值,无法感知异步变化,循环可能永远不会退出;右例将其声明为volatile,编译器每次访问都会重新从内存加载,因此正确。