写博客的目的第一是做笔记,第二是在错误的基础上不断刷新认知,这两天会写三篇关于嵌入式容易混淆的知识点,有错误欢迎拍砖!
1、volatile关键字的使用
关于volatile 关键字,如果你的理解仅仅是讲“是从内存直接取数据”,那实际上你对他理解还差一些火候。
实际上使用volatile 关键字声明变量的时候,编译器对访问的变量的代码不再进行优化。注意,这个关键字针对的对象是编译器,告诉编译器对该变量访问不进行优化。而导致的结果是我们看到的对该变量访问会直接访问内存,从而提供对变量的稳定访问。
未使用volatile修饰的变量,编译器可能优化读取和存储。可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,就会出现不一致的现象。注意,我这里说的是“寄存器”,没有强调是不是缓存(编译器翻译成汇编指令可以操作指定的寄存器进行优化,但是并不能操作缓存进行优化,对于内存地址来说缓存自身进行支配),下面会说到为什么和缓存没有关系。先说下编译器优化。
1.1 优化
为了提高程序的运行效率,会进行包括硬件和软件在内的优化。
硬件级别的优化:处理引入cache机制外,现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令硬件可以乱序执行,以充分利用利用CPU的指令流水线,提高执行速度。
软件级别的优化:软件的优化一种是写代码时由程序员优化,另一种是由编译器进行的优化。编译器优化的常用方法有:将内存变量缓存到寄存器;调整指令顺序以充分利用CPU指令流水线,常见的是重新排序读写指令。
(由编译器优化或者硬件进行的重新排序引起的问题解决思路是必须在必须以特定顺序执行的操作之间设置内存屏障(memory barrier),其中linux提供了一个宏来解决编译器的执行顺序问题,但是硬件顺序没法解决。void Barrier(void)这个函数用来通知编译器插入一个内存屏障,对硬件无效,编译后的代码会把当前CPU中所有修改过的数据存入内存,需要这些数据的时序再重新从内存中读出)。
特别需要记住,C 编译器是没有线程概念的,感受不到异步事件对该变量的操作。也就是同样一个变量在多线程(中断也可以理解为一个线程),编译优化后的程序一个线程或任务一个函数操作的是寄存器该变量的副本,另一个线程或者中断操作的一个函数可能是变量的内存或cache。
1.2 使用Volatile的几种场景
首先要说明的是,这些场景不使用volatile限定就一定会出错吗,不一定。首先,volatile 是告诉编译器防优化的,和编译器的优化等级有关(优化等级高,可能出错的概率高);其次,在编译器优化时,由于寄存器数量有限,未加限定的变量不一定被优化到了寄存器(通过寄存器操作的数据“备份”)。通常的情况就是没有volatile可以正常运行,修改了编译器的优化级别之后就又不能正常运行了。debug版本正常,但是release版本却不能正常的问题。
1)存储器映射的硬件寄存器通常要加volatile说明,因为每次对存储器映射的硬件寄存器读或写都可能有不同意义。
对于读操作,无论是读的可能是编译器优化取出的寄存器的“副本”或者读的cache,都存在实际外部输入是直接改变的内存,而寄存器或者cache是未感知的。比如说我们从指定的内存映射寄存器地址读取串口接收FIFO。
对应写操作,我们可能就是要通过寄存器实现对外部硬件控制。如:
0x12345678=0x55;
0x12345678=0x56;
0x12345678=0x57;
0x12345678=0x58;
表示对外部接口执行4个不同动作,而编译器优化会认为只执行第4条语句。
在1)这种场景下,除了编译器优化外,由于有cache的存在,也可能导致读写操作的是cache而使得实际读写未感知外部变化或者输出问题。我们可以理解为虽然volatile只是告诉编译器直接操作内存,但是实际上也把cache的问题跳过解决掉了。
2)中断服务函数中修改共其他程序监测的变量需要加volatile限定:
比如一个函数读操作一个变量A,中断处理函数写操作变量A。对于编译器由于没有线程概念(中断处理函数可以理解为特殊的线程),不会考虑到异步事件,编译器判断一个函数中没有修改过A,因此只执行一次A到寄存器的读操作,然后每次都只使用A在寄存器的副本。而中断处理函数中修改了A的值(实际内存或cache),该函数读操作寄存器“副本”就会得到错误结果。
比如下面是一个O2级别的优化:
这段汇编代码是:
wait:
mov eax, DWORD PTR busy[rip]
.L2:
test eax, eax
jne .L2
ret
busy:
.long 1
其中L2这一段即为while循环,这段指令是经过编译器优化的,可以看到,决定能否跳出循环是通过检查寄存器eax来完成的,而没有检查变量busy所在内存的真实内容。
加入volatile限定busy后的编译器生成汇编代码如下:
wait:
.L2:
mov eax, DWORD PTR busy[rip]
test eax, eax
jne .L2
ret
busy:
.long 1
注意看此时L2这一段,每次都从busy变量所在的内存中读取数据并存放在eax,然后再去判断,这样就能确保每次都能读取到busy变量的最新值。
3)多任务环境下各任务间共享的标志应该加volatile:
该情况和2)的原理实际上是一样的,就是编译器是不知道有异步事件改变变量的。2)和3)的情况都可能是由于一个任务操作的是在寄存器中的副本,一个操作的是实际内存或cache导致的不一致问题。
这个说个题外话,2)和3)不一致的原因是由于编译器优化的是寄存器。
思考:假如说编译器不优化(即没有寄存器副本引入的问题),在开启cache的情况下,线程间A和线程B都操作不加volatile限定,会有什么问题吗?答案是不会,因为即使在cache中,线程A或者线程B在使用变量a是发现cache有该变量,都会直接从cache中拿来使用,a变量对于A线程和B线程并不会有什么不一致问题,专门这么说只是用来告诉大家,volatile引入的不一致与cache没有半毛钱关系,只是与编译器优化的执行顺序(如1)中写操作)或寄存器副本( 1)读2)3) )相关。
1.3 Volatile 和原子化操作有关系吗
先说答案,Volatile和原子操作没有半毛钱关系,意思就是原本是原子操作加上它还是原子的,原本不是原子操作加上它也仍然是非原子的。对应本身具有原子性单个变量的读/写加上它也是原子的,对于类似于i++这种复合操作加上它限定也仍然不具有原子性。所以原子话得操作还需要进行加锁处理。
关于内容,明天补充。。。