为什么volatile
在 C 中需要?它是干什么用的?它会做什么?
18 回答
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
只读取一次的值,然后进入无限循环。那不是你想要的。
解决这个问题的方法是将指针声明gadget
为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
因此,每次遇到给定变量时,它都会生成代码以从主内存中获取给定变量的值。使用这种机制是因为任何时候操作系统或任何中断都可以修改该值。因此,使用volatile
将帮助我们每次都重新访问该值。
另一个用途volatile
是信号处理程序。如果你有这样的代码:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
允许编译器注意到循环体没有触及quit
变量并将循环转换为while (true)
循环。即使quit
在信号处理程序上为SIGINT
and设置了变量SIGTERM
;编译器无法知道这一点。
但是,如果quit
声明了变量volatile
,编译器每次都被迫加载它,因为它可以在其他地方修改。这正是您在这种情况下想要的。
volatile
告诉编译器您的变量可能会通过其他方式更改,而不是访问它的代码。例如,它可能是一个 I/O 映射的内存位置。如果在这种情况下未指定,则可以优化某些变量访问,例如,可以将其内容保存在寄存器中,并且不会再次读回内存位置。
请参阅 Andrei Alexandrescu 的这篇文章,“ volatile - 多线程程序员最好的朋友”
volatile关键字旨在防止在存在某些异步事件的情况下可能导致代码不正确的编译器优化。例如,如果您将原始变量声明为 volatile,则不允许编译器将其缓存在寄存器中——如果该变量在多个线程之间共享,这种常见的优化将是灾难性的。所以一般规则是,如果您有必须在多个线程之间共享的原始类型变量,请将这些变量声明为volatile. 但是你实际上可以用这个关键字做更多的事情:你可以用它来捕捉不是线程安全的代码,你可以在编译时这样做。本文展示了它是如何完成的;该解决方案涉及一个简单的智能指针,它还可以轻松序列化代码的关键部分。
本文适用于C
和C++
。
另请参阅 Scott Meyers 和 Andrei Alexandrescu 的文章“ C++ 和双重检查锁定的危险”:
所以在处理一些内存位置时(例如内存映射端口或由 ISR [中断服务例程] 引用的内存),必须暂停一些优化。volatile 存在用于指定对此类位置的特殊处理,具体而言:(1)volatile 变量的内容是“不稳定的”(可以通过编译器未知的方式更改),(2)对 volatile 数据的所有写入都是“可观察的”,因此它们必须认真执行,并且 (3) 对 volatile 数据的所有操作都按照它们在源代码中出现的顺序执行。前两条规则确保正确的读写。最后一个允许实现混合输入和输出的 I/O 协议。这是 C 和 C++ 的 volatile 保证的非正式内容。
我的简单解释是:
在某些场景下,编译器会根据逻辑或代码,对它认为不会改变的变量进行优化。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 寄存器和定位特定内存位置。
请参阅带有汇编列表的示例。 回复:在嵌入式开发中使用 C“volatile”关键字
我将提到挥发性物质很重要的另一种情况。
假设您对文件进行内存映射以获得更快的 I/O,并且该文件可以在后台更改(例如,该文件不在您的本地硬盘驱动器上,而是由另一台计算机通过网络提供服务)。
如果您通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,那么编译器生成的代码可以在您不知道的情况下多次获取相同的数据。
如果该数据碰巧发生了变化,您的程序可能会使用两个或多个不同版本的数据并进入不一致的状态。如果它处理不受信任的文件或来自不受信任位置的文件,这不仅会导致程序的逻辑错误行为,而且还会导致可利用的安全漏洞。
如果您关心安全性,并且应该考虑,这是一个需要考虑的重要场景。
当您想强制编译器不优化特定代码序列(例如,用于编写微基准测试)时,易失性也很有用。
volatile 表示存储可能随时更改并且会更改,但超出用户程序的控制范围。这意味着如果您引用该变量,程序应始终检查物理地址(即映射的输入 fifo),而不是以缓存的方式使用它。
在 Dennis Ritchie 设计的语言中,对任何对象的每次访问,除了地址未被获取的自动对象外,都会表现得好像它计算了对象的地址,然后读取或写入该地址的存储。这使得该语言非常强大,但严重限制了优化机会。
虽然可以添加一个限定符,让编译器假设特定对象不会以奇怪的方式更改,但这种假设适用于 C 程序中的绝大多数对象,并且它会为适合这种假设的所有对象添加限定符是不切实际的。另一方面,一些程序需要使用一些这样的假设不成立的对象。为了解决这个问题,标准规定编译器可能会假设未声明的对象不会volatile
以编译器无法控制的方式观察或更改其值,或者超出编译器的合理理解范围。
因为不同的平台可能有不同的方式可以在编译器的控制之外观察或修改对象,所以这些平台的高质量编译器应该在它们对volatile
语义的精确处理方面有所不同。不幸的是,由于该标准未能建议用于平台上低级编程的高质量编译器应该volatile
以能够识别该平台上特定读/写操作的任何和所有相关影响的方式处理,因此许多编译器未能做到因此,以一种高效但不能被编译器“优化”破坏的方式处理后台 I/O 之类的事情变得更加困难。
简单来说,它告诉编译器不要对特定变量进行任何优化。映射到设备寄存器的变量由设备间接修改。在这种情况下,必须使用 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;
}
只有编译器在编译后的代码中才能保证before gadget->data = data
only 。gadget->command = command
在运行时,处理器仍可能根据处理器架构重新排序数据和命令分配。硬件可能会得到错误的数据(假设小工具映射到硬件 I/O)。数据和命令分配之间需要内存屏障。
一个 volatile 可以从编译后的代码外部更改(例如,一个程序可能将一个 volatile 变量映射到一个内存映射寄存器。)编译器不会对处理 volatile 变量的代码应用某些优化 - 例如,它不会t 将其加载到寄存器而不将其写入内存。这在处理硬件寄存器时很重要。
正如许多人在这里正确建议的那样, volatile 关键字的流行用途是跳过对 volatile 变量的优化。
在阅读 volatile 后想到的最好的优点是——防止在longjmp
. 非本地跳转。
这是什么意思?
它只是意味着在您进行堆栈展开后将保留最后一个值,以返回到先前的堆栈帧;通常在某些错误情况下。
由于它超出了这个问题的范围,我不会在setjmp/longjmp
这里详细介绍,但值得一读;以及如何使用波动率特征来保留最后一个值。
它不允许编译器自动更改变量的值。volatile 变量用于动态使用。