前言
在开发一个产品的时候,需要在中断中和主循环中操作同一个变量。测试发现会不定期出现数据异常的情况。
排查发现这里是由于在对一个数据进行操作的时候受到了中断的影响。
问题分析
当主程序与中断共用一个变量的时候,可能发生下面的情况:
- 当主函数对变量的读-写,操作的时候,可能造成中断对变量的 读-写 操作无效:
例如:当主函数刚刚将变量读入到内部寄存器时,还未回写到变量中时,发生中断,这里中断中修改了
变量中的数据。当主函数将数值再写回到变量中的时候,就会造成中断对数值变量的改写无效。
- 多字节变量读取
当变量的其中一个字节读入到寄存器中时,发生中断,中断改写了变量的值。当中断返回时,变量的其
他字节继续被读入到寄存器中,造成旧字节组合错误。
主函数与中断函数共享变量的问题类似于在两个线程之间共享数据的问题,如何解决数据的冲突是系统设
计的关键。
解决方法
- volatile 声名的正确使用
- 注意原子操作,(关中断 -> 写变量 -> 开中断)
或者通过引入一个相同类型的临时变量,操作数据前先同步这两个数据
操作系统中对这种问题有另一种解决办法,即引入一个与ms_counter相同类型的临时变量,读取与更新
变量分开操作。
volatile 变量的用法
volatile是 易变的、不稳定的意思。在c语言中几乎很少使用这个关键字。这个关键自可以处理部分的
数据不一致的问题。这里对这个关键字的用法作一个梳理。
volatile关键字的本意是由于寄存器的访问速度要快于RAM,所以编译器一般都会减少去存取外部RAM的
优化,但是有可能会读取到脏数据。当要求使用 volatiale 声明变量的值的时候,系统总是重新从它所
在的内存中读取数据,即使它前面的指令刚刚从该处读取过数据,并且读取的数据会被立即保存。
准确的说就是优化器在用到这个变量的时候每次都需要小心的重新从内存中读取这个变量的值,而不是使
用保存在寄存器中数据的备份。
一般来说,volatile关键字用在如下几个地方;
- 中断服务程序中修改的提供给其他程序检测的变量需要加volatile;
- 多任务环境下各个任务之间共享的标志应该加上volatile;
- 存储器映射的硬件寄存器也要加上volatile说明,因为每次对它读写都会存在会造成不同的意思;
volatile 关键字和const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素
更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就
不再进行优化,从而可以提供对特殊地址的稳定访问。
如在下面的例子中:
1 | int i = 10; |
这时候编译器对代码进行优化,因为在(1)、(2)两条语句中,i 没有被用作左值,这时编译器认为i的值
没有被改变。所以在(1)语句时从内存中取出i的值给j之后这个值并没有被丢弃,而是在(2)语句使用时
继续使用这个值给k赋值,编译器这时不会生成汇编代码重新从内存中取i的值,这样的编译优化有利于提高
程序的运行效率。但是要注意:(1)、(2)两条语句之间的i并没有被用作左值才行。
这里再来看一下使用volatile声明变量的情况;
1 | volatile int i = 10; |
这里volatile 关键字告诉编译器i是随时可能发生变化的,每次使用这个变量时都必须从内存中重新取出
i的值,因而编译器生成的汇编代码会重新从i的地址处读取数据放到k中。
如果i是一个寄存器变量或者表示一个端口数据或者是多个线程共享数据,这样容易出错的情况,就可以
使用volatile关键字保证对特殊地址的稳定访问。
C语言原子操作 VS 非原子操作
共享内存中的原子操作是指一个指令是否完成一个线程相关的单步操作。当一个原子存储作用于一个共享
变量时,其他线程不能检测到这个未完成修改的值。当一个原子加载作用于一个共享变量时,它读取到一
个完整的值,就像此时出现一个单独的时刻,而非原子加载和存储则不能做到这些保证。
如果没有这些保证,无锁编程将无法实现,因为你不能使同一个线程同时操作一个共享变量。我们这里可
以制定如下规则:
任何时刻两个线程同时操作一个共享变量,当其中一个线程为写操作时,这两个线程必须使用原子操作。
如果这里没有使用原子,就会发生数据竞争,出现未定义的行为。这种数据竞争会导致读写的撕裂。
在c和c++中,所有的操作都被认定为非原子操作,甚至是普通的32位整数赋值,除非被别的编译器或者
硬件供应商指定。