为什么volatile
C需要?它是干什么用的?它会做什么?
Volatile告诉编译器不要优化与volatile变量有关的任何东西.
使用它只有一个原因:当您与硬件接口时.
假设您有一小块硬件映射到某处的RAM,并且有两个地址:命令端口和数据端口:
typedef struct { int command; int data; int isbusy; } MyHardwareGadget;
现在你要发送一些命令:
void SendCommand (MyHardwareGadget * gadget, int command, int data) { // wait while the gadget is busy: while (gadget->isbusy) { // do nothing here. } // set data first: gadget->data = data; // writing the command starts the action: gadget->command = command; }
看起来很简单,但它可能会失败,因为编译器可以自由地更改写入数据和命令的顺序.这将导致我们的小工具发出具有先前数据值的命令.还要看看忙碌循环时的等待.那个将被优化.编译器会尝试聪明,只读一次isbusy的值,然后进入无限循环.那不是你想要的.
解决这个问题的方法是将指针小工具声明为volatile.这样编译器就会被迫做你写的.它不能删除内存分配,它不能在寄存器中缓存变量,也不能改变赋值顺序:
这是正确的版本:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data) { // wait while the gadget is busy: while (gadget->isbusy) { // do nothing here. } // set data first: gadget->data = data; // writing the command starts the action: gadget->command = command; }
volatile
在C中实际上是为了不自动缓存变量的值而存在的.它会告诉机器不要缓存此变量的值.因此,volatile
每当遇到它时,它将从主存储器中获取给定变量的值.使用此机制是因为OS可以在任何时候修改该值或任何中断.所以使用volatile
将帮助我们每次重新获取价值.
volatile
信号处理程序的另一个用途.如果您有这样的代码:
int quit = 0; while (!quit) { /* very small loop which is completely visible to the compiler */ }
允许编译器注意到循环体不接触quit
变量并将循环转换为while (true)
循环.即使quit
变量是在信号处理程序上设置的SIGINT
和SIGTERM
; 编译器无法知道这一点.
但是,如果quit
声明了变量volatile
,则每次都强制编译器加载它,因为它可以在别处修改.在这种情况下,这正是您想要的.
volatile
告诉编译器您的变量可能通过其他方式更改,而不是访问它的代码.例如,它可以是I/O映射的存储器位置.如果在这种情况下没有指定,则可以优化一些变量访问,例如,其内容可以保存在寄存器中,并且存储器位置不会再次读回.
请参阅Andrei Alexandrescu撰写的这篇文章," volatile - 多线程程序员最好的朋友 "
该挥发性关键字进行设计,以防止可能使代码在某些异步事件的存在不正确的编译器优化.例如,如果声明一个原始变量作为 挥发性,编译器不允许它缓存在一寄存器-共同优化如果该变量被多个线程之间共享,这将是灾难性的.所以一般规则是,如果你有必须在多个线程之间共享的基本类型的变量,则声明那些变量volatile.但是你可以用这个关键字做更多的事情:你可以用它来捕获非线程安全的代码,你可以在编译时这样做.这篇文章展示了它是如何完成的; 该解决方案涉及一个简单的智能指针,也可以轻松地序列化关键代码段.
本文适用于C
和C++
.
另请参阅Scott Meyers和Andrei Alexandrescu 撰写的文章" C++和双重锁定的危险 ":
因此,与一些存储器位置打交道时(由ISR引用例如存储器映射的端口或存储器[中断服务例程]),某些优化必须暂停.挥发性存在用于指定特殊处理用于这样的位置,特别是:(1)挥发性变量的内容是"不稳定"(可以改变由装置未知的编译器),(2)所有写入到易失性数据是"可观察",所以他们必须以宗教的方式执行,以及(3)对易失性数据的所有操作都按照它们在源代码中出现的顺序执行.前两条规则确保正确的阅读和写作.最后一个允许实现混合输入和输出的I/O协议.这是非正式的C和C++的易变性保证.
我的简单解释是:
在某些情况下,基于逻辑或代码,编译器将对其认为不会更改的变量进行优化.所述volatile
关键字防止被优化的变量.
例如:
bool usb_interface_flag = 0; while(usb_interface_flag == 0) { // execute logic for the scenario where the USB isn't connected }
从上面的代码中,编译器可能认为usb_interface_flag
定义为0,而在while循环中它将永远为零.优化后,编译器会一直对待它while(true)
,导致无限循环.
为了避免这种情况,我们将标志声明为volatile,我们告诉编译器这个值可能被外部接口或程序的其他模块改变,即请不要优化它.那是volatile的用例.
volatile的边际用途如下.假设您要计算函数的数值导数f
:
double der_f(double x) { static const double h = 1e-3; return (f(x + h) - f(x)) / h; }
问题是由于舍入误差x+h-x
通常不等于h
.想一想:当你减去非常接近的数字时,会丢失很多有效数字,这可能会破坏导数的计算(想想1.00001-1).可能的解决方法是
double der_f2(double x) { static const double h = 1e-3; double hh = x + h - x; return (f(x + hh) - f(x)) / hh; }
但是根据您的平台和编译器开关,该功能的第二行可能会被积极优化的编译器消灭.所以你写了
volatile double hh = x + h; hh -= x;
强制编译器读取包含hh的内存位置,从而丧失最终的优化机会.
有两种用途.这些在嵌入式开发中经常被特别使用.
编译器不会优化使用volatile关键字定义的变量的函数
易失性用于访问RAM,ROM等中的确切存储器位置......这种情况更常用于控制存储器映射设备,访问CPU寄存器和定位特定存储器位置.
查看汇编列表的示例. Re:在嵌入式开发中使用C"volatile"关键字
当您想强制编译器不优化特定代码序列时(例如,用于编写微基准测试),易失性也很有用.
我会提到另一种情况,即挥发物很重要.
假设您对文件进行内存映射以获得更快的I/O,并且该文件可以在后台更改(例如,该文件不在本地硬盘驱动器上,而是由另一台计算机通过网络提供).
如果通过指向非易失性对象的指针(源代码级别)访问内存映射文件的数据,那么编译器生成的代码可以多次获取相同的数据,而不会发现它.
如果该数据发生变化,您的程序可能会使用两个或更多不同版本的数据并进入不一致状态.这不仅会导致程序的逻辑错误行为,而且如果它处理来自不受信任位置的不受信任文件或文件,也会导致其中存在可利用的安全漏洞.
如果你关心安全性,那么这是一个需要考虑的重要方案.
volatile意味着存储可能随时更改并被更改,但不受用户程序控制之外的限制.这意味着如果引用变量,程序应始终检查物理地址(即映射的输入fifo),而不是以缓存方式使用它.
维基说出一切volatile
:
易变(计算机编程)
Linux内核的doc也是一个很好的表示法volatile
:
为什么不应该使用"volatile"类型类
我认为,您不应期望太多volatile
。为了说明这一点,请看Nils Pipenbrinck极受好评的答案中的示例。
我想说,他的榜样不适合volatile
。volatile
仅用于:
防止编译器进行有用且理想的优化。与线程安全,原子访问甚至内存顺序无关。
在该示例中:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data) { // wait while the gadget is busy: while (gadget->isbusy) { // do nothing here. } // set data first: gadget->data = data; // writing the command starts the action: gadget->command = command; }
在gadget->data = data
之前gadget->command = command
仅只有在编译器编译代码的保证。在运行时,关于处理器体系结构,处理器仍可能对数据和命令分配进行重新排序。硬件可能会获得错误的数据(假设小工具已映射到硬件I / O)。在数据和命令分配之间需要内存屏障。
在丹尼斯·里奇(Dennis Ritchie)设计的语言中,对任何对象的每次访问(未使用其地址的自动对象除外)都将表现为好像计算了对象的地址,然后在该地址读取或写入了存储。这使得该语言非常强大,但是优化机会却非常有限。
虽然可能可以添加一个限定符来邀请编译器假定一个特定的对象不会以怪异的方式进行更改,但是这种假定将适用于C程序中的绝大多数对象。在这样的假设适用的所有对象上添加限定符是不切实际的。另一方面,某些程序需要使用某些对象,而这些对象对此假设将不成立。为解决此问题,标准指出,编译器可能会假设未声明的对象volatile
将不会以其编译器无法控制的方式或合理的编译器无法理解的方式来观察或更改其值。
由于各种平台可能具有不同的方式,可以在编译器的控制范围之外观察或修改对象,因此适合这些平台的高质量编译器应在volatile
语义的精确处理方面有所不同。不幸的是,由于该标准未能建议打算在平台上进行低级编程的高质量编译器应volatile
以能够识别该平台上特定读/写操作的所有及所有相关影响的方式进行处理,因此许多编译器无法做到这一点因此,这种方式使得以高效的方式处理诸如后台I / O之类的事情变得更加困难,但不会被编译器的“优化”破坏。
简单来说,它告诉编译器不要对特定变量进行任何优化。映射到设备寄存器的变量由设备间接修改。在这种情况下,必须使用挥发物。