C语言中volatile关键字的作用
注:本文内容来源于冀博博文,感谢作者的整理。
volatile详解
volatile的本意是“易变的” ,因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。
当要求使用volatile声明变量值的时候,==系统总是重新从它所在的内存读取数据==,即使它前面的指令刚刚从该处读取过数据。
精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
事例
告诉==compiler(编译器)==不能做任何优化
- 比如要往某一个地址发送两条指令
1 | int *ip =...; //设备地址 |
- 以上程序compiler可能做优化而成:
1 | int *ip = ...; |
- 结果第一个指令丢失。如果用volatile, compiler就不允许做任何的优化,从而保证程序的原意:
1 | volatile int *ip = ...; |
即使你要compiler做优化,它也不会把两次付值语句间化为一。它只能做其它的优化。
用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能重复使用放在cache或寄存器中的备份。
1 | volatile char a; |
如果没有 volatiledoother()不会被执行
使用volatile变量的几个场景
中断服务程序中修改的供其它程序检测的变量需要加volatile;
1 | static int i=0; |
程序的本意是希望ISR_2中断产生时,在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
多任务环境下各任务间共享的标志应该加volatile
存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。
- 例如:假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
1 | int *output = (unsigned int *)0xff800000;//定义一个IO端口; |
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译编译的代码结果相当于:
1 | int init(void) |
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
- 例如:
1 | volatile int *output=(volatile unsigned int *)0xff800000;//定义一个I/O端口 |
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中禁止任务调度,3中则只能依靠硬件的良好设计。
几个问题
- 一个参数既可以是const还可以是volatile吗?
可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。 - 一个指针可以是volatile 吗?
可以,当一个中服务子程序修该一个指向一个buffer的指针时。
volatile的本质:
编译器的优化
在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。
volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人。
下面的函数有什么错误:
1 | int square(volatile int *ptr) |
该程序的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
1 | int square(volatile int *ptr) |
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
1 | long square(volatile int *ptr) |
注意:频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。