volatile本质为“直接存取原始内存地址”,即每次访问时都直接访问原始内存地址
假设有一个外部硬件设备,通过某个内存映射寄存器与你的程序通信。该寄存器的地址是0x4000
,用来标识设备是否准备好发送数据。
例子1:不使用volatile
int *deviceReady = (int *)0x4000;
void waitForDevice() {
while (*deviceReady == 0) {
// 等待设备准备好
}
// 处理设备数据
}
编译器看到上面的代码会认为deviceReady
的值在循环中并没有改变,因此可能优化该代码,使其只读取一次内存地址0x4000
的值。这样的话,即使外部硬件改变了该地址的值,程序还是无法检测到。
例子2:使用volatile
volatile int *deviceReady = (int *)0x4000;
void waitForDevice() {
while (*deviceReady == 0) {
// 等待设备准备好
}
// 处理设备数据
}
通过将指针声明为volatile
,我们告诉编译器不要对其进行优化,即不要假设deviceReady
的值在连续的访问之间保持不变。每次循环时,都会从内存地址0x4000
重新读取值。
从底层数据变化上的区别
-
不使用
volatile
:编译器可能只读取一次内存地址0x4000
的值,并将其缓存。即使外部硬件更改了该地址的值,程序也无法感知。这可能导致程序一直卡在循环中,无法检测到设备已经准备好。 -
使用
volatile
:编译器每次循环都会重新从地址0x4000
读取值,不会进行任何优化。这样,外部硬件更改该地址的值时,程序能立即检测到,并能正确地响应设备的准备状态。
这个简单的例子展示了volatile
如何确保程序与外部世界的正确交互,特别是在硬件访问和多线程环境中。
volatile的作用就是使变量具有灵活性,如上所示在不使用volatile情况下编译器会默认该指针所指向内存的值是不变的(但其实该变量因为硬件上的变化已经发生了改变)导致该部分内存的变化一直未被检测到。但是使用了volatile之后就能够检测到该内存的变化了(因为编译器未对该语句优化,使得在每次查询该指针时都会去查对应的寄存器内容)。
volatile 的本质就是告诉编译器不要对设计到它的语句进行编译优化。
再举几个例子:
1.在中断中被改值的变量在主函数中也用到该变量
2.多进程/多任务两个进程分别都使用到了同一个变量,如果该变量没有volatile那么一个进程中若在对该变量进行检测时任务进行了切换切换后的进程对该变量进行更改,再换回后的进程理应检测到另一个进程的更改,但却因为没加volatile导致编译器对变量进行了优化默认在该进程中该变量无变化,所以最终检测不到变化。
3.存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。
例如:
假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
int * output = (unsigned int * )0xff800000;//定义一个IO端口;
int init(void)
{
int i;
for(i=0;i< 10;i++){
* output = i;
}
}
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译编译的代码结果相当于:
int init(void)
{
* output = 9;
}
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。