C/C++ volatile


代码编译运行环境:VS2017+Win32+Windows7 64bits。

volatile 是“易变的”、“不稳定”的意思。volatile 是 C 语言中一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题。

1.volatile 的作用

定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。

在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。

而在多任务环境中,虽然在一个函数体内部,在两次读取变量之间没有对变量的值进行修改,但是该变量仍然有可能被其他的程序(如中断程序、另外的线程等)所修改。如果这时还是从寄存器而不是从RAM中读取,就会出现被修改了的变量值不能得到及时反应的问题。如下程序对这一现象进行了模拟。

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
	int i=10;
	int a=i;
	cout<<a<<endl;
	_asm
	{
		mov dword ptr [ebp-4],80
	}
	int b=i;
	cout<<b<<endl;
}

程序在VS2017环境下生成Release版本,输出结果是:

10
10

阅读以上程序,注意以下几个要点:
(1)以上代码必须在Release模式下考查,因为只有Release模式下才会对程序代码进行优化,而这种优化在变量共享的环境下容易引发问题。

(2)在语句b=i;之前,已经通过内联汇编代码修改了i的值,但是i的变化却没有反映到b中,如果i是一个被多个任务共享的变量,这种优化带来的错误很可能是致命的。

(3)汇编代码 [ebp-4] 表示变量i的存储单元,因为 ebp 是扩展基址指针寄存器,存放函数所属栈的栈底地址,先入栈,占用 4 个字节。随着函数内申明的局部变量的增多,esp(栈顶指针寄存器)就会相应的减小,因为栈的生长方向由高地址向低地址生长。i 为第一个变量,栈空间已被ebp 入栈占用了 4 个字节,所以i的地址为 ebp-i,[ebp-i] 则表示变量 i 的存储单元。

那如何抑制编译器对读取变量的这种优化,来防止错误读取呢?volatile 可以轻松胜任,将上面的程序稍作修改,将变量 i 申明为 volatile 即可,观察如下程序:

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
	volatile int i=10;
	int a=i;
	cout<<a<<endl;
	_asm
	{
		mov dword ptr [ebp-4],80
	}
	int b=i;
	cout<<b<<endl;
	getchar();
}

程序输出结果为:

10
80

也就是说,第二次读取变量 i 的值的时候,已经获得了变化之后的值。跟踪汇编代码可知,凡是申明为 volatile 的变量,每次都是从内存中读取变量的值,而不是在某些情况下直接从寄存器中取值。

2.volatile 应用场景

(1)并行设备的硬件寄存器(如状态寄存器)。
假设要对一个设备进行初始化,此设备的某一个寄存器为 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通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。

(2)一个中断服务子程序中访问到的变量;

static int i=0;

int main()
{
	while(1)
	{
		if(i) dosomething();
	}
}

/* Interrupt service routine */
void IRS()
{
	i=1;
}

上面示例程序的本意是产生中断时,由中断服务子程序 IRS 响应中断,变更程序变量 i,使在 main 函数中调用 dosomething 函数,但是,由于编译器判断在 main 函数里面没有修改过 i,因此可能只执行一次对从 i 到某寄存器的读操作,然后每次 if 判断都只使用这个寄存器里面的“i 副本”,导致 dosomething 永远不会被调用。如果将变量 i 加上 volatile 修饰,则编译器保证对变量 i 的读写操作都不会被优化,从而保证了变量 i 被外部程序更改后能及时在原程序中得到感知。

(3)多线程应用中被多个任务共享的变量。
当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入 CPU 寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。示例如下:

volatile  bool bStop=false;  //bStop 为共享全局变量  
//第一个线程
void threadFunc1()
{
	...
	while(!bStop){...}
}

//第二个线程终止上面的线程循环
void threadFunc2()
{
	...
	bStop = true;
}

要想通过第二个线程终止第一个线程循环,如果 bStop 不使用 volatile 定义,那么这个循环将是一个死循环,因为 bStop 已经存到寄存器,寄存器中 bStop 的值永远不会变成 false,加上 volatile,程序在执行时,每次均从内存中读出 bStop 的值,就不会死循环了。

是否了解 volatile 的应用场景是区分 C/C++ 程序员和嵌入式开发程序员的有效办法,搞嵌入式的家伙们经常同硬件、中断、RTOS 等打交道,这些都要求用到 volatile 变量,不懂得 volatile 将会带来程序设计的灾难。

3.volatile 常见问题

下面的问题可以看一下面试者是不是直正了解 volatile。
(1)一个参数既可以是 const 还可以是 volatile 吗?为什么?
可以。一个例子是只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。它是 const 因为程序不应该试图去修改它。

(2)一个指针可以是 volatile 吗?为什么?
可以。尽管这并不常见。一个例子是当一个中断服务子程序修该一个指向一个buffer的指针时。

(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 可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr) 
{ 
	int a=*ptr; 
	return a * a; 
} 

4.嵌入式编程中 volatile 的作用

嵌入式编程中经常用到 volatile 这个关键字,常见用法可以归结为以下两点:

(1)告诉 compiler 不能做任何优化。比如要往某一地址送两条指令。

 int *ip =...;  //设备地址 
 *ip = 1;       //第一条指令 
 *ip = 2;       //第二条指令 

以上程序 compiler 可能会优化成:

int *ip = ...; 
*ip = 2; 

结果第一个指令丢失。如果使用 volatile, compiler 就不允许做任何的优化,从而保证程序的原意:

volatile int *ip = ...; 
*ip = 1; 
*ip = 2; 

(2)volatile 定义的变量如果在程序外被改变,每次都必须从内存中读取,而不能把他放在 cache 或寄存器中重复使用。如:

volatile char a;   
a=0; 
while(!a)
{ 
	  //do some things;   
}   
doother(); 

如果没有 volatile,doother() 不会被执行。

volatile 能够避免编译器优化带来的错误,但使用 volatile 的同时,也需要注意频繁地使用 volatile 很可能会增加代码尺寸和降低性能,因此要合理地使用 volatile。


参考文献

[1] 陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.
[2] C中的volatile用法
[3] ebp与esp讲解
[4] C语言再学习 – 关键字volatile
[5] C语言中volatile关键字的作用

  • 13
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值