volatile 影响编译器编译的结果,指出 volatile 变量每次使用时都需要去内存里重新读取它的值,而一旦修改此变量就直接写入内存,而不是操作cache中的变量。 volatile 变量有关的运算,不要进行特殊优化。
{
int I = 10;
int j = I;
int k=I;
}
对于上面的代码段,优化的作法是,编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会从 cache 中把上次读的数据 i 放在 k 中。而不是重 新从 i 的内存里面读。这样以来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问,不会出 错。
我们来看另一个例子
int volatile I;
while(i)
{
do_something();
}
如果 i 没有被 volatie 修饰,当 while 循环执行时,另一段程序并发的执行了 i=0 ,这个循环仍不会退出,因为每次循环都是检查寄存器中的值。 如果有 volatie 修饰,那么循环结束,因为循环每次检查 i 的时候,会先从内存把 i 读入寄存器,这个时候 i 在其它地方被赋 0 ,则循环结束。
对于程序中存在多个执行流程访问同一全局变量的情况, volatile 限定符是必要的。
此外,虽然程序只有单一的执行流程,但是变量属于以下情况之一的,也需要 volatile 限定。
1 变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样
2 即使多次向变量的内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的
什么样的内存单元会具有这样的特性呢?肯定不是普通的内存,而是映射到内存地址空间的硬件寄存器,例如串口的接收寄存器属于上述第一种情况,而发送寄存器属于上述第二种情况。
因此建议使用 volatile 变量的场所:
1 并行设备的硬件寄存器
2 一个中断服务子程序中会访问到的非自动变量(全局变量)
3 多线程应用中被几个任务共享的变量
4 longjmp跳回之后需要保持修改的变量
#include <setjmp.h> #include <stdio.h> jmp_buf jmpbuf; void f2() { longjmp(jmpbuf,1); } void f1(int i,int j,int k) { printf("in f1 count = %d val =%d sum = %d /n",i,j,k); f2(); } int main() { int count; register int val; volatile int sum; count = 2,val=3,sum=4; if(setjmp(jmpbuf)!=0) { printf("after jump:count = %d val =%d sum = %d /n",count,val,sum); return 0; } count = 97,val =98,sum = 99; f1(count,val,sum); }
上面的代码分别在非优化和优化条件下将得到不同的结果。
在嵌入式或多线程的环境下,不懂得 voliatile 可能会导致灾难。我们通过下面几个问题来加深对其理解。
关键字 volatile 有什么含意 ? 并给出三个不同的例子
1) 一个参数既可以是 const 还可以是 volatile 吗?解释为什么。
是的。一个例子是只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。它是 const 因为程序不应该试图去修改它。(也就是说, const 指定了我们的程序代码中是不可以改变这个变量的, 但是 volatile 指出,可以是由于硬件的原因,在代码意外更改这个值,但是我们的代码同时会更新使用这个最新的数值)
2) 一个指针可以是 volatile 吗?解释为什么。
是的。一个例子是当一个中服务子程序修该一个指向一个 buffer 的指针时。(把指针声明为 volatile 的类型,可以保证指针所指向的地址随时发生变化)
3) 下面的函数有什么错误:
int square(volatile int *ptr) {
return *ptr * *ptr;
}
这段代码的目的是用来返指针 *ptr 指向值的平方,但是,由于 *ptr 指向一个 volatile 型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr){
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于 *ptr 的值可能被意想不到地该变,因此 a 和 b 可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
int square(volatile int *ptr){
int a;
a = *ptr;
return a * a;
}
在 signal.h 头文件中声明了一种 sig_atomic_t 的类型,当把变量声明为该类型是,则会保证该变量在使用或赋值时, 无论是在 32 位还是 64 位的机器上都能保证操作是原子的, 它会根据机器的类型自动适应。 通常情况下, int 类型的变量通常是原子访问的,在 linux 里这样定义: typedef int __sig_atomic_t; 另外 gnu c 的文档也说比 int 短的类型通常也是具有原子性的,例如 short 类型。同时,指针(地址)类型也一定是原子性的。在 32 位的平台下,访问 64 位的变量,就不是原子的,而需要两次读内存操作。 sig_atomic_t 类型的变量应该总是加上 volatile 限定符 ,因为要使用 sig_atomic_t 类型的理由也正是要加 volatile 限定符的理由。