猪哥 的个人资料猪哥的Blog日志列表留言簿 工具 帮助
2007/10/25

Volatile的陷阱

最近写的关于在嵌入式开发中常遇到的关于volatile关键字使用的短文,都是些通用的技术,贴上来share。另外,禁止转载。
 
对于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类型的结构体!!

所以这个代码应该改写成这样:

typeof 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 = ...; ①
bdp->buffer[i] = ...; ②

请问上面标记了①和②的两行代码,哪个是确实在访问volatile对象,而哪个又是undefined的结果?

答案是:②是volatile的,①是undefined。来看本例的数据结构示意图:

        (non-volatile)
bdp -->+-------------+
       |             |
       |   ... ...   |
       |             |
       +-------------+    (volatile)   
       |    buffer   |-->+------------+
       +-------------+   |            |
                         |            |
                         |            |
                         +------------+
                         |  buffer[i] |
                         +------------+
                         |            |
                         |            |
                         +------------+

buffer成员本身是通过一个non-volatile的指针bdp访问的,按照C99标准的定义,这就属于undefined的情况,因此对bdp->buffer的访问编译器不一定能保证是volatile的;

虽然buffer成员本身可能不是volatile的变量,但是buffer成员是一个指向volatile对象的指针。因此对buffer成员所指对象的访问编译器可以保证是volatile的,所以bdp->buffer[i]是volatile的。

所以,看似简单的volatile关键字,用起来还是有非常多的讲究在里面的,大家一定要引起重视。

乱序执行和内存屏障

最近写的一些关于在驱动程序开发中会遇到的关于乱序执行问题的短文,都是些通用的技术,贴上来share。另外,禁止转载。
ps:这玩意原本是用Docbook写得,转过来还真是麻烦~~

处理器的乱序和并发执行

目前的高级处理器,为了提高内部逻辑元件的利用率以提高运行速度,通常会采用多指令发射、乱序执行等各种措施。现在普遍使用的一些超标量处理器通常能够在一个指令周期内并发执行多条指令。处理器从L1 I-Cache预取了一批指令后,就会分析找出那些互相没有关联可以并发执行的指令,然后送到几个独立的执行单元进行并发执行。比如下面这样的代码(假定编译器不做优化):

z = x + y;
p = m + n;

CPU就有可能将这两行无关代码分别送到两个算术单元去同时执行。像Freescale的MPC8541这种嵌入式处理器一个指令周期能够加载4条指令、发射2条指令到流水线、用5个独立的执行单元来并发执行。

通常来说访存指令(由LSU单元执行)所需要的指令周期可能很多(可能要几十甚至上百个周期),而一般的算术指令通常在一个指令周期就搞定。所以有 可能代码中的访存指令耗费了多个周期完成执行后,其他几个执行单元可能已经把后面有多条逻辑上无关的算术指令都执行完了,这就产生了乱序。

另外访存指令之间也存在乱序的问题。高级的CPU可以根据自己Cache的组织特性,将访存指令重新排序执行。访问一些连续地址的可能会先执行,因 为这时候Cache命中率高。有的还允许访存的Non-blocking,即如果前面一条访存指令因为Cache不命中,造成长延时的存储访问时,后面的 访存指令可以先执行以便从Cache取数。对写指令的访存乱序有可能造成的错误后果,所以处理器通常有专门的机制(通常是做了个缓冲)保证在出现异常或者 错误的时候,可以丢弃异常点后面的写指令的结果不做写入。

处理器的分支预测功能也能引起并发执行。处理器的分支预测单元有可能直接把两条分支的指令都预取来一块并发执行掉。等到分支判断的结果出来以后,再丢弃错误分支的计算结果。这样在很多情况下可以实现0周期跳转。比如这样的代码(假定编译器不做优化):

z = x + y; 
if (z > 0) then
p = m + n;
else
p = m - n;

看上去如果z不计算出来是无法继续的。但是实际上CPU有可能先把三个加法都同时进行计算,然后根据z=x+y的结果直接挑选正确的p值。

因此,即使是从汇编上看顺序正确的指令,其执行的顺序也是不可预知的。处理器能够保证并发和乱序执行不会得到错误结果,但是如果是对一些硬件寄存器 的操作不能允许乱序的话,程序员就必须把这个情况告诉CPU。告诉的方法就是通过CPU提供的一组同步指令实现,通常在CPU的文档里面有对同步指令的使 用说明。系统函数库里面的内存屏障(rmb/wmb/mb)实际上也是通过这些同步指令实现的。因此在C编码的时候,只要设置好内存屏障,就能告诉CPU 哪些代码是不能乱序的。

编译器的乱序优化

受到处理器预取单元的能力限制,处理器每次只能分析一小块指令的并发性,如果指令相隔比较远就无能为力了。但是从编译器的角度来看,编译器能够对很 大一个范围的代码进行分析,能够从更大的范围内分辨出可以并发的指令,并将其尽量靠近排列让处理器更容易预取和并发执行,充分利用处理器的乱序并发功能。 所以现代的高性能编译器在目标码优化上都具备对指令进行乱序优化的能力。并且可以对访存的指令进行进一步的乱序,减少逻辑上不必要的访存,以及尽量提高 Cache命中率和CPU的LSU(load/store unit)的工作效率。所以在打开编译器优化以后,看到生成的汇编码并不严格按照代码的逻辑顺序是正常的。和处理器一样,如果想要告诉编译器不要去对某些 指令乱序优化,也要通过一些方式来告诉编译器。通常可以通过volatile关键字来抑制(注意,不是禁止)编译器对相关变量的访问优化。举个例子:

int *p, *q; 
......;
*p = 1;
*p = 2;
*q = *p;

这样,编译器通常会优化掉前面一个对*p的写入(逻辑上冗余),仅对*p写入2。而对*q赋值的时候,编译器认为此时*q的结果就应该是上次*p的值,会优化掉从*p取数的过程,直接把在寄存器中保存的*p的值给*q(PowrPC汇编):

(假设r3=p,r4=q) 
li r5, 2 // r5赋值2
stw r5, 0(r3) // 把r5写到*p
stw r5, 0(r4) // 把r5写到*q

但是如果为p指针加上了volatile关键字,情况就不同了:

volatile int *p; 
int *q;
......;
*p = 1;
*p = 2;
*q = *p;

在这种情况下,编译器看见*p是volatile的时候,就会:

  1. 不对*p操作生成乱序指令(通常如此,具体请看后面的解释)

  2. 每次从*p取数据的时候,一定会进行一次访存操作,哪怕前面不久才取过*p的值放在寄存器里。

  3. 不合并对*p的写操作(也只是通常如此,解释见后)

所以这回的结果如下(PowrPC汇编):

(假设r3=p,r4=q) 
li r5, 1 // r5赋值1
stw r5, 0(r3) // 把r5写到*p
li r5, 2 // r5赋值2
stw r5, 0(r3) // 把r5写到*p
lwz r5, 0(r3) // 从*p取值到r5
stw r5, 0(r4) // 把r5写到*q

这样编译器会在汇编码级别保证指令有序和不优化掉访存操作。通常简单地使用volatile关键字就可以解决编译器的乱序问题,但是这些指令到了处理器执行的时候,仍然可能被乱序。对于处理器乱序执行的避免就需要用到一组内存屏障函数(barrier)了。

重要

绝大多数的编译器,通常不会优化掉对volatile对象的访问,并且通常保持同一个volatile对象的一系列读写操作是有序的(但是不能保证不同的volatile对象之间有序)。

但是,这不是绝对的。因为ANSI C99标准关于对volatile对象访问时编译器是否要绝对保证禁止乱序(reorder)和禁止访问合并(combine access)并没有做任何规定!仅仅是鼓励编译器最好不要去优化对volatile对象的访问,而唯一的强制要求仅仅是要求编译器保证对volatile对象的访问优化不会跨越“sequence point”即可(所谓sequence point是指一些诸如外部函数调用、条件或循环跳转等关键点,具体定义请查阅C99标准内的详细说明)。

这就是说,如果一个编译器在两个sequence point之间像对待普通变量一样去优化volatile变量,也是完全符合C99标准的!比如:

volatile int a;

if (...) { ... }  // sequence point
a = 1;
a = 2;
a = 3;
printk("...");    // sequence point

在两个sequence point之间,要是有编译器对a的赋值操作合并(即仅写入3)或者乱序(如写1和写2对调),都是完全符合C99标准的。所以,我们在使用的时候,不能指望用了volatile以后绝对能生成有序的完整的汇编码,即不要指望volatile来保证访存有序。实质上 volatile最大的作用主要还是在保证每次使用从内存中取值,而并不能保证编译器不做其他任何优化(毕竟volatile从字面上看意思是“易变”而不是“有序”。编译器只保证对volatile对象即时更新但不保证访问有序也不是说不过去的)。

从另一个角度看,即使是编译器生成的汇编码有序,处理器也不一定能保证有序。就算编译器生成了有序的汇编码,到了处理器那里也拿不准是不是会按照代码顺序执行。所以就算编译器保证有序了,程序员也还是要往代码里面加内存屏障才能保证绝对访存有序,这倒不如编译器干脆不管算了,因为内存屏障本身就是一个sequence point,加入后已经能够保证编译器也有序。

因此,对于切实是需要保障访存顺序的代码,就算当前使用的编译器能够编译出有序的目标码来,我们也还是必须通过设置内存屏障的方式来保证有序,否则都是不严谨,有隐患的。

Barrier屏障函数

Barrier函数可以在代码中设置屏障,这个屏障可以阻挡编译器的优化,也可以阻挡处理器的优化。

对于编译器来说,设置任何一个屏障都可以保证:

  1. 编译器的乱序优化不会跨越屏障,即屏障前后的代码不会乱序;

  2. 在屏障后所有对变量或者地址的操作,都会重新从内存中取值(相当于刷新寄存器中的变量副本)。

而对于处理器来说,根据不同的屏障有不同的表现(以下仅仅列举3种最简单的屏障):

  1. 读屏障rmb()
    处理器对读屏障前后的取数指令(LOAD)能保证有序,但是不一定能保证其他算术指令或者是写指令的有序。对于读指令的执行完成时间也不能保证,即它不能保证在屏障之前的读指令一定都执行完成,只能保证屏障之前的读指令一定能在屏障之后的读指令之前完成。

  2. 写屏障wmb()
    处理器对屏障前后的写指令(STORE)能保证有序,但是不一定能保证其他算术指令或者是读指令的有序。对于写指令的执行完成时间也不能保证,即它不能保证在屏障之前的写指令一定都执行完成,只能保证屏障之前的写指令一定能在屏障之后的写指令之前完成。

  3. 通用内存屏障mb()
    处理器保障只有屏障之前的访存操作(包括读写)都完成以后才会执行屏障之后的访存操作。即可以保障读写之间的有序(但是同样无法保证指令完成的时 间)。这种屏障对处理器的执行单元效率产生的负面影响要比单纯用读屏障或者写屏障来的大。比如对于PowerPC来说这种通用屏障通常是使用sync指令实现的,在这种情况下处理器会丢弃所有预取的指令并清空流水线。所以频繁使用内存屏障会降低处理器执行单元的效率。

对于驱动开发者来说,一些对设备寄存器的操作,通常是必须保证有序的。在绝大部分情况下,一般都是写操作。对于有序的写操作,必须设置写屏障(wmb):

例:在驱动中使用写屏障

/* Mask out everything */ 
im_intctl->ic_simrh = 0x00000000;
im_intctl->ic_simrl = 0x00000000;
wmb();  
/* Ack everything */ 
im_intctl->ic_sipnrh = 0xffffffff; im_intctl->ic_sipnrl = 0xffffffff;

这是一个对中断控制器操作的例子。在设置两个mask寄存器的值的时候,这两个写操作没有顺序要求,因此可以不加屏障。但是对ack寄存器的设置必须在mask寄存器完成设置以后,所以在中间要加入写屏障wmb()以保证对两组寄存器的写有序。

同样的,对于一系列的只读操作,也可以简单使用rmb()来保证有序。


注意

任何一个rmb()或者wmb()都是可以被替换成mb()的。但是因为上面提到过的mb()的效率问题,所以应该只有在同时需要读屏障和写屏障的时候,才建议使用mb()。否则应该根据实际情况来选择合适的屏障。当然,在设备初始化的时候,即使是使用mb()也不会对性能带来什么影响,因为设备一般只会初始化一次。但是在发生很频繁的设备操作(比如网口的收发帧中断等)时,应该考虑到mb()对性能的影响。

如果驱动不仅仅需要在单纯的读指令或者写指令之间有序,还需要保证读写指令之间有序的时候,就需要设置mb()屏障了。下面将演示一个这样的例子:

例:使用mb()屏障保证读写有序

我们假设有一个设备,在读取设备信息时需要依次对REG1~3这三个寄存器进行写入操作(写入设备读取命令),然后才能依次读取REG4和REG5取得设备返回的信息。

REG1 = a; 
wmb(); // 保证REG1和REG2的写有序
REG2 = b;
wmb(); // 保证REG2和REG3的写有序
REG3 = c;
mb(); // 保证在对设备读之前,前面的配置操作都完成(读写之间有序)
*d = REG4;
rmb(); // 保证REG4和REG5的读有序
*e = REG5;
mb(); // 保证与未来对设备的操作有序
return;
  • 对于REG1~3的写入,可以通过设置写屏障来保证有序;

  • 在进行REG4和5的读取之前,因为得保证前面的寄存器写操作都执行完才能读,所以需要设置一个内存屏障mb()来保证前面对寄存器的写都完成,以保障读写指令之间的有序;

  • 后面两个读操作之间就可以通过设置读屏障来保证有序了;

  • 最后通常在从设备操作函数返回之前,我们一般需要保证对设备的操作都执行完毕了。这样下次对设备进行操作的时候我们可以保证设备已经完成了上次操作,避免反复调用设备操作函数带来的函数间的乱序问题。所以在最后设置一个内存屏障mb(),保障和未来对设备的其他访问有序。

进一步阅读

如果还想进一步了解内存屏障的有关信息,特别是关于多处理器系统中的内存屏障,可以阅读:

  • Linux内核源码附带的《LINUX KERNEL MEMORY BARRIERS》by David Howells <dhowells@redhat.com>

2007/10/1

无意发现MSN隐身还是能被发现的

今天上MSN的时候无意抓到几个状态为“Invisible”的家伙,大约显示了一分钟以后这些人又变成Offline了。下线再上线,又是这样:有几个人先Invisible,后来Offiline。联想到之前几周总是有人被我在上线时逮到处于这种状态的,怀疑难道是俺这MSN插件牛屄了能剥掉隐身人的外衣不成?!立马邀同学一试,果然灵光!!同学隐身后只要俺重新登录就能在短时间内看到处于Invisible状态的联系人哈哈。效果如下(Flickr图片,请用Firefox+Access Flickr查看):



红框处就是Invisible的联系人(注意状态是个小眼睛)。点开来可以聊天,标题栏状态也是显示Invisible:



不过过了一分钟左右,隐身的人又会变成Offline了。有时候隐身人的捕获会有些迟滞,具体原因还不清楚。不过俺多上上下下几次,基本能抓全(# ̄▽ ̄#)。

隐身行为就跟说谎一样完全不可饶恕,准备再进行一次大清理,将这些老喜欢隐身的家伙清除出我的联系人列表!严重鄙视在IM上隐身的人,完全是一种对人不尊重和欺骗的行为!!