volatile 的英文解释是——“易失的,易改变的”。
顾名思义,这个关键字的含义是向编译器指明变量的内容可能会由于编译器意想不到的情况的变化而发生变化。
这个解释仍然比较抽象,感兴趣的可以继续阅读下面内容。
先看一下编译器对程序的优化过程是怎么进行的
如果编译器在代码中发现对同一地址的两次访问之间,没有对该地址进行写操作,那么编译器的优化过程认为——第一次寻址读取该地址时取得的变量的值(编译器会尽最大可能把这个值存放在通用寄存器R中或者cache中)作为第二次寻址的值,而并不是再做第二次内存的 I/O 寻址操作(当CPU把该变量的值放到通用寄存器或者cache中中后就不会再关心对应内存中的值)。
例如:
int i = 10;
int j = i; // (1)语句
int k = i; // (2)语句
因为在(1)、(2)两条语句中,i没有被用作左值(同一地址的两次访问之间,没有对该地址进行写操作),这时候编译器认为i的值没有发生变化,所以在(1)语句时,从内存中取出i的值赋给j之后,这个值并没有丢掉(存放在通用寄存器中),而是在(2)语句时继续用这个值给k赋值。编译器不会生成汇编代码而重新从内存里寻址i的值。
Volatile这个关键字的必要性(真正意义之所在)
但其他程序(例如内核程序或一个中断)修改了内存中该变量的值,此时寄存器R中的值并不会随之改变而更新,由于优化器的作用编译器仍然去利用之前存放在寄存器R中的值,而不去寻址内存中的值(但我们必须改变这个变量的值)。
为了解决这种情况C语言就引入了volatile限定词,让代码在引用该变量时多费一点劲儿,再去内存中取出该变量的值。
例如:
Volatile int i = 10;
int j = i; // (3)语句
int k = i; // (4)语句
这里,volatile关键字告诉编译器i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从内存中i的地址处读取i的值存放在k中。
一句话概括就是,当用volatile关键字修饰变量时,优化器在用到这个变量时必须每次都小心地去内存重新读取(关键之处)这个变量的值,而不是使用保存在寄存器R里的备份。
Volatile和register的对比
volatile 跟以前的 register 相反。
register 告诉编译器尽量将变量放到寄存器中使用, 而volatile 强制将更改后的值写回内存。如果不写回内存,对于一些全局共享的变量,可能导致不一致问题。
volatie变量将和cache不发生关系,只和内存有关系
简单点说就是每次操作前从内存取值
有volatie修饰的变量,每次操作时遵循下面动作:
从内存取值 ---> 放入寄存器 ----> 操作 ----> 写回内存
没有volatie修饰的变量,操作可能遵循:
从内存取值 ---> 放入寄存器 ----> 第一次操作 -----> 第二次操作(此时仍操作寄存器中的值) …… ----> 第N次操作 ----> 写回内存
举个例子论述两者关系
int volatie i; //全局变量,在其它地方会被修改
while (i)
{
do_somethings();
}
如果i没有被volatie修饰,当while循环执行时,另一段程序并发的执行了i = 0, 这个循环仍不会退出,因为每次循环都是检查寄存器中的值。
如果有volatie修饰,那么循环结束,因为循环每次检查i的时候,会先从内存把i读入寄存器,这个时候i在其它地方被赋0,则循环结束。
Volatile关键字应用的三个地方
1、修改硬件寄存器,尤其是状态寄存器
大家都知道,在硬件级别,如果寄存器值自动改变了,编译器是不会主动发现的。
经过编译器的自动优化,我们读到的都是寄存器中存储的旧的状态寄存器的值, 而非最新的状态寄存器值。
例如:
#define STATUS (*(volatile unsigned long *)0x56000010)
2、多线程中被几个线程共享的变量
线程修改共享变量var是不会通知编译器的。
所以线程A坚持不懈地读着var在寄存器或者cache中的副本,读出来的内容是0,但很可惜,线程B早就把var变量给修改为1了。
鉴于此,我们必须加上volatile这个关键字来解决这个问题。
3、中断服务程序ISR当中用
ISR:中断服务程序 (interrupt service routine)
所谓中断是指当CPU正在处理某件事情的时候,外部发生的某一事件(如一个电平的变化,一个脉冲沿的发生或定时器计数溢出等)请求CPU迅速去处理,于是CPU暂时中止当前的工作,转去处理所发生的事件。中断服务处理完该事件以后,再回到原来被中止的地方继续原来的工作。
由volatile引出的三个问题
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2)一个指针可以是volatile 吗?解释为什么。
3)下面的函数有什么错误:
int square(volatile int *ptr)
{return *ptr * *ptr;}
答:
1)是的。一个典型的个例子就是只读的状态寄存器。
¨ 它是volatile因为它可能被意想不到地改变。
¨ 它是const因为程序不应该试图去修改它。
2)是的。尽管这并不很常见。
一个例子是当一个中服务子程序修改一个指向一个buffer的指针时。
3)这段代码的有个恶作剧。
这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,
编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a, b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。
结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}