vloatile
超详细!!!
单纯的C程序员肯能会很少用到volatile关键字,但是如果说想从事嵌入式的工作,如果不懂得使用volatile,那就基本和offer说拜拜了。本讲内容将详细讲述volatile,为什么要用,什么时候用,有什么注意点…安排!
答应我,硬着头皮也要看完!真的很详细!!!
问题引入
int obj = 10;
int a = 0;
int b = 0;
a = obj;
sleep(100);
b = obj;
//在上述程序中,编译器在编译时发现obj没有被当成左值使用,
//从而“聪明”的编译器会自作主张的将obj替换为10,从而a,b的值都将为10。
这么看来似乎编译器做的并没有什么不对的地方,但是,结合上图,如果说在这休眠的100ms内,正好有一个硬件中断发生,使得obj的值从10——>100,而我们在这里等待100ms的目的就是为了等待这个硬件中断的发生将obj的值从10——>100。这么了看来,编译器自作主张的举动背离了我们的想法。
volatile关键字
一个定义为volatile的变量是说这变量可能会被意想不到的改变,这样,编译器就不会去假设这个变量的值了。
编译器的优化:在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会把变量读取到一个寄存器中,以后再取变量值时,就直接从寄存器中取值;当变量值在本线程内改变时,会同时把变量的新值copy到该寄存器中,以便保值一致。
当变量因在别的线程中等待而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取到的值和实际的变量值不一致。当该寄存器因在别的线程等待而改变了值,原变量的值不会改变,从而造成应用程序读取到的值和实际的变量值不一致。
注:以上内容摘自:https://www.cnblogs.com/reality-soul/p/6140192.html
volatile可以理解为编译器警告指示字;
volatile用于告诉编译器必须每次去内存中取变量值(即:警告它别偷懒,别去取寄存器中的值);
volatile主要修饰可能被多个线程访问的变量;
volatile也可以修饰可能被未知因素更改的变量;
遇到volatile关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile的使用
- 并行设备的硬件寄存器(如:状态寄存器)
存储器映射的硬件寄存器通常要用volatile,因为每次对他的读写都有可能有不同的意义;相关寄存器定义时也会用到volatile,因为这些寄存器中的值是随时变化的,假设存在一个名叫AAA的寄存器,如果未用volatile修饰,那么在使用这个AAA寄存器的时候,会直接从CPU寄存器中取值,因为AAA寄存器之前被访问过,也就是之前就从内存中取出AAA的值保存到某个寄存器中,之所以直接从CPU寄存器中取值而不去内存中取值,是因为编译器优化代码的结果[访问CPU寄存器比访问RAM快得多] - 一个中断服务子程序中会访问到非自动变量
由于访问寄存器比RAM快得多,所以编译器会做减少存取外部RAM的优化,从而导致一些值的变化脱离程序原本的设计意义 - 多线程应用中被几个任务共享的变量
当两个线程中都要用到某一个变量且该变量的值会被改变时,如果不用volatile修饰,很有可能会导致一个线程使用内存中的变量,另一个使用寄存器中的变量,显然,程序会出错。
详细的volatile应用举例可参考大佬的文章:C语言再学习 – 关键字volatile.
简单来说就是让编译器每次操作该变量时一定要从内存中读取,而不是使用寄存器中的值!
面试问题
- 一般会问volatile的关键字的含义以及使用举例,答案就是上面的知识点
- const和volatile是否可以同时修饰一个变量?有什么含义?
可以,const表示变量只读,也就是不希望该变量被本程序改变,但如果该变量可能被其他程序程序改变,而本程序还在检测这个变量的值,这时就需要加上volatile。此时,变量具备const和volatile的双重属性。例如:
extern const volatile unsigned int rt_clock;
这是在RTOS系统内核中常见的一种声明:rt_clock通常是指系统时钟,它经常被时钟中断进行更新。所以它是volatile,因此在用的时候,要让编译器每次从内存里面取值。而rt_clock通常只有一个写者(时钟中断),其他地方对其的使用通常都是只读的。所以将其声明为 const,表示这里不应该修改这个变量。所以volatile和const是两个不矛盾的东西,并且一个对象同时具备这两种属性也是有实际意义的。 - 一个指针可以是 volatile 吗?
可以,当一个中断服务子程序修改一个指向一个buffer的指针时。例如:int* volatile ptr; 这里实际上是定义了一个int类型的指针,并且这个指针本身是volatile的,但是指针所指向的内容并不是volatile的,实际使用时,编译器对代码中指针变量ptr本身的操作不会进行优化,但是对ptr指向的内容却会作为non-volatile内容来处理,对*ptr的操作还是会被优化。这种写法一般出现在这个指针变量有可能会被中断函数修改的情形。将其定义为volatile以后,编译器每次取指针变量的值的时候都会从内存中载入,这样即使这个变量已经被别的程序修改了当前函数用的时候也能得到修改后的值。
到这里,你以为感觉这个volatile关键字掌握的差不多了?难受哦,还早!
volatile陷阱
例1:non-volatile类型的结构体指针访问被定义为volatile的结构体成员&volatile类型的结构体指针访问被定义为non-volatile的结构体成员
struct devregs{
unsigned short volatile csr;
unsigned short const volatile data;
};
struct devregs * const dvp = DEVADDR;
while ((dvp->csr & (READY | ERROR)) == 0)
; /* NULL - wait till done */
/*
程序本意是申明一个设备的硬件寄存器组,其中csr控制/状态寄存器,可以由
程序员向设备写入控制字,也可以由硬件设备设置反应其工作状态;data数据
寄存器只会由硬件来设置,由程序员来进行读操作;
*/
理论上来说程序并没有什么问题,也符合实际情况但是!!!仔细思考,通过一个non-volatile类型的结构体指针dvp去访问被定义为volatile的结构体成员,编译器将会做何处理?答案是:Undefined!C99 标准没有对编译器在这种情况下的行为做规定,因此编译器有可能正确的将dvp->csr作为volatile的变量来处理,程序正常;也有可能将dvp->csr作为non-volatile的变量来处理,从而执行while()时,编译器会自动优化为最开始的那一次的取值结果,也就是和前面描述的一样,不再从硬件寄存器中取值,导致程序有可能会陷入死循环,所以,这种使用方法是危险的!
但是,如果你用一个volatile的指针来指向一个非volatile的对象,就比如我们使用volatile类型的结构体指针访问被定义为non-volatile的结构体成员,这种情况下,volatile指针所指向的结构体的使用编译器认为是volatile的,即使原本那个对象没有被申明为volatile。
所以代码修改如下:
struct devregs{
unsigned short csr;
unsigned short const data;
};
volatile struct devregs * const dvp = DEVADDR;
总结:
non-volatile类型的结构体指针访问被定义为volatile的结构体成员(Dangerous)
volatile类型的结构体指针访问被定义为non-volatile的结构体成员(OK)
例2:
volatile struct devregs {
/* stuff */
} dev1;
......;
struct devregs dev2;
定义了一个volatile的结构体类型,以及一个这样的volatile结构体变量的dev1,那么请问,再定义一个dev2,这个dev2变量是不是volatile的?
答案:不是!第二次所定义的dev2变量实际上是non-volatile的,因为实际上在定义结构体类型时的volatile关键字修饰的是dev1这个变量,而不是struct devregs类型的结构体!!! 所以代码修改如下:
typedef volatile struct devregs {
/* stuff */
} devregs_t;
devregs_t dev1;
......;
devregs_t dev2;
例3:
/* DMA buffer descriptor */
struct bd {
unsigned int state;
unsigned char *data_buff;
};
struct devregs {
unsigned int csr;
struct bd *tx_bd;
struct bd *rx_bd;
};
volatile struct devregs * const dvp = DEVADDR;
/* send buffer */
dvp->tx_bd->state = READY;
while ((dvp->tx_bd->state & (EMPTY | ERROR)) == 0)
; /* NULL - wait till done */
结合例1,使用volatile类型的结构体指针访问被定义为non-volatile的结构体成员,这种情况下,volatile指针所指向的结构体的使用编译器认为是volatile的,即使原本那个对象没有被申明为volatile。dvp是volatile类型的,但是也只有其指向的devregs结构才属于volatile object的范围,也就是说,volatile类型的dvp指针可以保障其所指的volatile object中的结构体成员自身是volatile变量,但是并不能保障这个指针变量(如tx_bd)所指的数据成员也是volatile的,因为这个指针并没有申明为指向volatile数据的指针。
所以可以将struct devregs中的成员做如下修改,保对state的处理也是volatile的:
struct devregs {
unsigned int csr;
volatile struct bd *tx_bd;
volatile struct bd *rx_bd;
};
不过最为稳妥和清晰的办法还是这样:
volatile struct devregs * const dvp = DEVADDR;
volatile struct bd *tx_bd = dvp->tx_bd;
tx_bd->state = READY;
while ((tx_bd->state & (EMPTY | ERROR)) == 0)
; /* NULL - wait till done */
这样一眼就能够看出哪些数据结构的访问必须保证是volatile的。
例4:
struct hw_bd {
......;
volatile unsigned char * volatile buffer;
};
struct hw_bd *bdp;
......;
bdp->buffer = ...; ①
bdp->buffer[i] = ...; ②
问上面标记了①和②的两行代码,哪个是确实在访问volatile对象,而哪个又是undefined的结果?
结合本题的数据结构示意图以及例1,non-volatile类型的结构体指针bdp访问结构体成员buffer是会被认为Undefined!因此,对bdp->buffer的访问编译器不一定能保证是volatile的,虽然buffer成员本身可能不是volatile的变量,但是buffer成员是一个指向volatile对象的指针。因此对buffer成员所指对象的访问编译器可以保证是volatile的,所以bdp->buffer[i]是volatile的。
以上关于volatile陷阱的内容摘选自大佬的一篇非常好的文章Volatile的陷阱.
<未完,持续更新…>