1、概述
尽管有大量C语言的文献,但是 “volatile” 关键字在某种程度上还是不能被很好地理解(甚至是有经验的C程序员)。究其原因,是在用高级语言编写的典型C程序中,没有 “volatile” 变量的真实用例。基本上,除非你用C语言进行一些低级硬件编程,否则你可能不会使用被限定为 “volatile” 的变量。所谓低级编程,值得是一段C代码,它处理外围设备、IO端口(主要是内存映射的IO端口)、与硬件交互的终端服务例程(ISR)。这就是为什么很难有一个简单的C程序能直接地展示 “volatile” 关键字的确切效果。
事实上,本文中,如果我们能够解释 “volatile” 的含义和目的,这将为进一步研究和使用C中的 “volatile” 奠定基础。要理解 “volatile”,首先需要了解编译器对C程序的作用。在高层,我们知道编译器将C代码转换为机器代码,这样可执行文件就可以在没有实际源代码的情况下运行。与其他技术类似,编译器技术也有很大的发展。在将源代码转换为机器代码时,编译器通常会尝试优化输出,以便最终执行较少的机器代码。这样的一个优化是删除访问变量的不必要的机器代码,从编译器的角度来看,这些代码不会改变。假设有如下的代码:
uint32 status = 0;while (status == 0)
{/*Let us assume that status isn't being changed in this while loop or may be in our whole program*//*So long as status (which could be reflecting status of some IO port) is ZERO, do something*/
}
优化编译器会发现 while 循环中并不会修改 status。因此,不需要在每次循环迭代后一次又一次地访问 status 变量。因此编译器会将这个循环转换为一个无限循环,即 while(1)
,这样读取 status 的机器代码就不需要了。请注意,编译器不知道 status 是一个特殊的变量,它可以在任何时间点从当前程序外部进行更改,例如,在外围设备上发生了一些IO操作,设备IO端口被内存映射到此变量。所以,实际上,我们希望编译器在每次循环迭代后访问 status 变量,即使它没有被编译器正在编译的程序修改。
有人可能会说,我们可以关闭此类程序的所有编译器优化,这样我们就不会遇到这种情况。这不是一种好的方法,因为诸多原因,例如
A)每个编译器实现都是不同的,因此它不是一个可移植的解决方案
B) 仅仅因为一个变量,我们不想关闭编译器在程序的其他部分所做的所有其他优化
C)关闭所有优化,我们的低级别程序无法按预期工作,例如程序大小(size)增加过多或延迟执行
这就是 “volatile” 存在的意义。基本上,我们需要告诉编译器 status 是特殊变量,不允许对该变量进行优化。有了这个,我们可以这样定义我们的变量:
volatile uint32 status = 0;
为了解释简单,选择上面的例子。但通常,volatile 用于指针,如下所示:
volatile uint32 * statusPtr = 0xF1230000
这里,statusPtr 指向一个内存位置(如一些IO端口),其中的内容可以在任何时间点从一些外围设备更改。请注意,我们的程序可能无法控制或知道内存何时会改变。所以将其设为 “volatile”,这样编译器就不会对 statusPtr 指向的 可变 变量进行优化了。
在我们讨论 “volatile” 的上下文中,引用了C语言标准即ISO/IEC 9899 C11 - 第6.7.3条
“An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.”
“A volatile declaration may be used to describe an object corresponding to a memory-mapped input/output port or an object accessed by an asynchronously interrupting function. Actions on objects so declared shall not be ‘‘optimized out’’ by an implementation or reordered except as permitted by the rules for evaluating expressions.”
简单地说,C标准说 “volatile” 变量能从程序的外部更改,这就是为什么编译器不应该优化它们的访问。现在,你可以猜测,程序中有太多的 “volatile” 变量也会导致编译器的优化越来越少。我们希望它给你足够的背景知识,让你了解 ”volatile“ 的含义和目的。
从本文中,希望您去掉 “volatile变量–>不要对该变量进行编译器优化” 的概念!
2、实例
volatile 关键字的目的是防止编译器以编译器无法确定的方式对可能发生变化的对象进行任何优化。
被声明为 volatile 的对象在优化中被省略,因为它们的值可以随时被当前代码范围之外的代码更改。系统总是从内存位置读取 volatile 对象的当前值,而不是在请求时将其值保存到临时寄存器中,即使先前的指令曾向同一个对象请求过该值。因为,简单的问题是,变量的值是如何以编译器无法预测的方式变化?考虑以下情况来回答这个问题:
1)全局变量被范围外的终端服务例程修改:例如,全局变量可以表示将动态更新的数据端口(通常是全局指针,称为内存映射IO)。读取数据端口的代码必须声明为volatile,以便获取端口上可用的最新数据。如果变量未能声明为volatile,那么将导致编译器优化代码,使其只读取端口一次,并在临时寄存器中使用相同的值来加快程序速度(速度优化)。通常,当新数据可用而出现中断时,ISR用于更新这些数据端口。
2)多线程应用程序中的全局变量:线程的通信有多种方式,即消息传递、共享内存、邮箱等。全局变量是共享内存的弱形式。当两个线程通过全局变量共享信息时,这些变量需要用 volatile 进行限定。由于线程是异步执行的,因此由一个线程引起的全局变量的任何更新都应该由另一个使用者线程立即获取。编译器可以读取全局变量,并将它们放置在当前线程上下文的临时变量中。为了消除编译器优化的效果,需要将此类全局变量限定为volatile。
如果不使用 volatile 限定符,就会产生下面的问题:
1)启用优化后,代码可能无法按预期执行
2)启用和使用中断时,代码可能无法按预期执行
来看个例子了解编译器如何解释 volatile 关键字。考虑下面的代码。我们正在使用指针修改const对象的值,且正在编译没有优化选项的代码。因此,编译器不会进行任何优化,并且会修改 const 对象的值。
/* Compile code without optimization option */
#include <stdio.h>
int main(void)
{const int local = 10;int *ptr = (int*) &local;printf("Initial value of local : %d \n", local);*ptr = 100;printf("Modified value of local: %d \n", local);return 0;
}
当我们使用 gcc 的 “–save temps” 选项编译代码时,它会生成 3 个输出文件:
1)预处理代码(有 .i
扩展名)
2)汇编代码 (有 .s
扩展名)
3)目标代码(有 .o
扩展名)
在没有优化的情况下编译代码,这就是为什么汇编代码的大小会更大。
输出:
[narendra@ubuntu]$ gcc volatile.c -o volatile –save-temps
[narendra@ubuntu]$ ./volatile
Initial value of local : 10
Modified value of local: 100
[narendra@ubuntu]$ ls -l volatile.s
-rw-r–r– 1 narendra narendra 731 2016-11-19 16:19 volatile.s
[narendra@ubuntu]$
使用优化选项(即 -O
选项)编译相同的代码。如下代码中,“local” 被声明为 const(非volatile)。GCC编译器进行优化并忽略试图更改const 对象值的指令。因此,const对象的值保持不变。
/* Compile code with optimization option */
#include <stdio.h>int main(void)
{const int local = 10;int *ptr = (int*) &local;printf("Initial value of local : %d \n", local);*ptr = 100;printf("Modified value of local: %d \n", local);return 0;
}
对于上面的代码,编译器会进行优化,这就是汇编代码的大小会减少的原因。
输出:
[narendra@ubuntu]$ gcc -O3 volatile.c -o volatile –save-temps
[narendra@ubuntu]$ ./volatile
Initial value of local : 10
Modified value of local: 10
[narendra@ubuntu]$ ls -l volatile.s
-rw-r–r– 1 narendra narendra 626 2016-11-19 16:21 volatile.s
将 const 对象声明为 volatile 并用优化选项编译代码。尽管编译代码时使用了优化选项,但是const对象的值依然会改变,因为变量被声明为了 volatile,这意味着不做任何优化。
/* Compile code with optimization option */
#include <stdio.h>int main(void)
{const volatile int local = 10;int *ptr = (int*) &local;printf("Initial value of local : %d \n", local);*ptr = 100;printf("Modified value of local: %d \n", local);return 0;
}
输出:
[narendra@ubuntu]$ gcc -O3 volatile.c -o volatile –save-temp
[narendra@ubuntu]$ ./volatile
Initial value of local : 10
Modified value of local: 100
[narendra@ubuntu]$ ls -l volatile.s
-rw-r–r– 1 narendra narendra 711 2016-11-19 16:22 volatile.s
[narendra@ubuntu]$
上面的例子可能不是一个很好的实际例子,但其目的是解释编译器如何解释 volatile 关键字。作为一个实际的例子,想想手机上的触摸传感器。提取触摸传感器的驱动程序将读取触摸的位置并将其发送到更高级别的应用程序。驱动程序本身不应修改(const-ness)读取位置,并确保每次读取触摸输入时都是新的(volatile-ness)。这样的驱动器必须以 const volatile
的方式读取触摸传感器输入。
注意:以上代码是特定于编译器的,可能不适用于所有编译器。这些例子的目的是让读者理解这个概念。