对于 volatile 关键字, 大部分的 C 语言教材都是一笔带过, 并没有做太过深入的分析, 所以这里简单整理了一些关于 volatile 的使用注意事项. 实际上从语法上来看 volatile 和 const 是一样的, 但是如果 const 用错, 几乎不会有什么问题; 而 volatile 用错, 后果可能很严重. 所以在 volatile 的使用上, 建议大家还是尽量求稳, 少用一些没有切实把握的技巧.
注意 volatile 修饰的是谁
首先来看下面两个定义的区别:
uchar * volatile reg; 这行代码里 volatile 修饰的是 reg 这个变量. 所以这里实际上是定义了一个 uchar 类型的指针, 并且这个指针变量本身是 volatile 的. 但是指针所指的内容并不是 volatile 的! 在实际使用的时候, 编译器对代码中指针变量 reg 本身的操作不会进行优化, 但是对 reg 所指的内容 *reg 却会作为 non-volatile 内容处理, 对 * reg 的操作还是会被优化. 通常这种写法一般用在对共享指针的声明上, 即这个指针变量有可能会被中断等函数修改. 将其定义为 volatile 以后, 编译器每次取指针变量的值的时候都会从内存中载入, 这样即使这个变量已经被别的程序修改了当前函数用的时候也能得到修改后的值(否则通常只在函数开始取一次放在寄存器里, 以后就一直使用寄存器内的副本).
volatile uchar *reg; 这行代码里 volatile 修饰的是指针所指的内容. 所以这里定义了一个 uchar 类型的指针, 并且这个指针指向的是一个 volatile 的对象. 但是指针变量本身并不是 volatile 的. 如果对指针变量 reg 本身进行计算或者赋值等操作, 是可能会被编译器优化的. 但是对 reg 所指向的内容 *reg 的引用却禁止编译器优化. 因为这个指针所指的是一个 volatile 的对象, 所以编译器必须保证对 * reg 的操作都不被优化. 通常在驱动程序的开发中, 对硬件寄存器指针的定义, 都应该采用这种形式.
volatile uchar * volatile reg; 这样定义出来的指针就本身是个 volatile 的变量, 又指向了 volatile 的数据内容.
volatile 与 const 的合用
从字面上看, volatile 和 const 似乎是一个对象的两个对立属性, 是互斥的. 但是实际上, 两者是有可能一起修饰同一个对象的. 看看下面这行声明:
extern const volatile unsigned int rt_clock; 这是在 RTOS 系统内核中常见的一种声明: rt_clock 通常是指系统时钟, 它经常被时钟中断进行更新. 所以它是 volatile, 易变的. 因此在用的时候, 要让编译器每次从内存里面取值. 而 rt_clock 通常只有一个写者(时钟中断), 其他地方对其的使用通常都是只读的. 所以将其声明为 const, 表示这里不应该修改这个变量. 所以 volatile 和 const 是两个不矛盾的东西, 并且一个对象同时具备这两种属性也是有实际意义的.
注意
在上面这个例子里面, 要注意声明和定义时对 const 的使用:
在需要读写 rt_clock 变量的中断处理程序里面, 应该如下定义 (define) 此变量:
volatile unsigned int rt_clock; 而在提供给外部用户使用的头文件里面, 可以将此变量声明 (declare) 为:
extern const volatile unsigned int rt_clock; 这样是没有问题的. 但是切记一定不能反过来, 即定义一个 const 的变量:
const unsigned int a; 但是却声明为非 const 变量:
extern unsigned int a; 这样万一在用户函数里面对 a 进行了写操作, 结果是 Undefined.
再看另一个例子:
volatile struct devregs * const dvp = DEVADDR; 这里的 volatile 和 const 实际上是分别修饰了两个不同的对象: volatile 修饰的是指针 dvp 所指的类型为 struct devregs 的数据结构, 这个结构对应者设备的硬件寄存器, 所以是易变的, 不能被优化的; 而后面的 const 修饰的是指针变量 dvp. 因为硬件寄存器的地址是一个常量, 所以将这个指针变量定义成 const 的, 不能被修改.
危险的 volatile 用法
下面将列举几种对 volatile 的不当使用和可能导致的非预期的结果.
例: 定义为 volatile 的结构体成员
考察下面对一个设备硬件寄存器结构类型的定义:
- struct devregs{
- unsigned short volatile csr;
- unsigned short const volatile data;
}; 我们的原意是希望声明一个设备的硬件寄存器组. 其中有一个 16bit 的 CSR 控制 / 状态寄存器, 这个寄存器可以由程序向设备写入控制字, 也可以由硬件设备设置反映其工作状态. 另外还有一个 16bit 的 DATA 数据寄存器, 这个寄存器只会由硬件来设置, 由程序进行读入.
看起来, 这个结构的定义没有什么问题, 也相当符合实际情况. 但是如果执行下面这样的代码时, 会发生什么情况呢?
- struct devregs * const dvp = DEVADDR;
- while ((dvp->csr & (READY | ERROR)) == 0)
; /* NULL - wait till done */ 通过一个 non-volatile 的结构体指针, 去访问被定义为 volatile 的结构体成员, 编译器将如何处理? 答案是: Undefined!C99 标准没有对编译器在这种情况下的行为做规定. 所以编译器有可能正确地将 dvp->csr 作为 volatile 的变量来处理, 使程序运行正常; 也有可能就将 dvp->csr 作为普通的 non-volatile 变量来处理, 在 while 当中优化为只有开始的时候取值一次, 以后每次循环始终使用第一次取来的值而不再从硬件寄存器里读取, 这样上面的代码就有可能陷入死循环!!
如果你使用一个 volatile 的指针来指向一个非 volatile 的对象. 比如将一个 non-volatile 的结构体地址赋给一个 volatile 的指针, 这样对 volatile 指针所指结构体的使用都会被编译器认为是 volatile 的, 即使原本那个对象没有被声明为 volatile. 然而反过来, 如果将一个 volatile 对象的地址赋给一个 non-volatile 的普通指针, 通过这个指针访问 volatile 对象的结果是 undefined, 是危险的.
所以对于本例中的代码, 我们应该修改成这样:
- struct devregs {
- unsigned short csr;
- unsigned short data;
- };
volatile struct devregs * const dvp = DEVADDR; 这样我们才能保证通过 dvp 指针去访问结构体成员的时候, 都是作为 volatile 来处理的.
例: 定义为 volatile 的结构体类型
考察如下代码:
- volatile struct devregs {
- /* stuff */
- } dev1;
- ......;
struct devregs dev2; 作者的目的也许是希望定义一个 volatile 的结构体类型, 然后顺便定义一个这样的 volatile 结构体变量 dev1. 后来又需要一个这种类型的变量, 因此又定义了一个 dev2. 然而, 第二次所定义的 dev2 变量实际上是 non-volatile 的!! 因为实际上在定义结构体类型时的那个 volatile 关键字, 修饰的是 dev1 这个变量而不是 struct devregs 类型的结构体!!
所以这个代码应该改写成这样:
- typedef volatile struct devregs {
- /* stuff */
- } devregs_t;
- devregs_t dev1;
- ......;
devregs_t dev2; 这样我们才能得到两个 volatile 的结构体变量.
例: 多次的间接指针引用
考察如下代码:
- /* 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 */ 这样的代码常用在对一些 DMA 设备的发送 Buffer 处理上. 通常这些 Buffer Descriptor(BD)当中的状态会由硬件进行设置以告诉软件 Buffer 是否完成发送或接收. 但是请注意, 上面的代码中对 dvp->tx_bd->state 的操作实际上是 non-volatile 的! 这样的操作有可能因为编译器对其读取的优化而导致后面陷入死循环.
因为虽然 dvp 已经被定义为 volatile 的指针了, 但是也只有其指向的 devregs 结构才属于 volatile object 的范围. 也就是说, 将 dvp 声明为指向 volatile 数据的指针可以保障其所指的 volatile object 之内的 tx_bd 这个结构体成员自身是 volatile 变量, 但是并不能保障这个指针变量所指的数据也是 volatile 的(因为这个指针并没有被声明为指向 volatile 数据的指针).
要让上面的代码正常工作, 可以将数据结构的定义修改成这样:
- struct devregs {
- unsigned int csr;
- volatile struct bd *tx_bd;
- volatile struct bd *rx_bd;
}; 这样可以保证对 state 成员的处理也是 volatile 的. 不过最为稳妥和清晰的办法还是这样:
- 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 的.
例: 到底哪个 volatile 可能无效
就在你看过前面几个例子, 感觉自己可能已经都弄明白了的时候, 请看最后这个例子:
- struct hw_bd {
- ......;
- volatile unsigned char * volatile buffer;
- };
- struct hw_bd *bdp;
- ......;
- bdp->buffer = ...; 1
bdp->buffer= ...; 2请问上面标记了1和2的两行代码, 哪个是确实在访问 volatile 对象, 而哪个又是 undefined 的结果?
答案是:2是 volatile 的,1是 undefined. 来看本例的数据结构示意图:
- (non-volatile)
- bdp -->+-------------+
- | |
- | ... ... |
- | |
- +-------------+ (volatile)
- | buffer |-->+------------+
- +-------------+ | |
- | |
- | |
- +------------+
- |buffer|
- +------------+
- | |
- | |
+------------+buffer 成员本身是通过一个 non-volatile 的指针 bdp 访问的, 按照 C99 标准的定义, 这就属于 undefined 的情况, 因此对 bdp->buffer 的访问编译器不一定能保证是 volatile 的;
虽然 buffer 成员本身可能不是 volatile 的变量, 但是 buffer 成员是一个指向 volatile 对象的指针. 因此对 buffer 成员所指对象的访问编译器可以保证是 volatile 的, 所以 bdp->buffer 是 volatile 的.
所以, 看似简单的 volatile 关键字, 用起来还是有非常多的讲究在里面的, 大家一定要引起重视.
来源: https://www.2cto.com/kf/201810/784245.html