为什么 C 中需要 volatile 关键字?

为什么在C中需要volatile ?它的用途是什么?它会做什么?

378902 次浏览

volatile告诉编译器你的变量可以通过其他方式改变,而不是访问它的代码。例如,它可能是一个I/ o映射的内存位置。如果在这种情况下没有指定这一点,一些变量访问可以被优化,例如,它的内容可以保存在寄存器中,并且内存位置不会再次读入。

C语言中的volatile实际上是为了不自动缓存变量的值而存在的。它会告诉编译器不要缓存这个变量的值。因此,每次遇到给定的volatile变量时,它都会生成代码从主存中获取该变量的值。之所以使用这种机制,是因为该值在任何时候都可以被操作系统或任何中断修改。因此,使用volatile将帮助我们每次都重新访问该值。

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;
}

看起来很简单,但可能会失败,因为编译器可以随意更改数据和命令的写入顺序。这将导致我们的小工具使用之前的数据值发出命令。还可以看看busy循环中的wait。这个会被优化掉。编译器会尽量聪明,只读取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变量可以从编译代码的外部进行更改(例如,程序可以将volatile变量映射到内存映射寄存器)。编译器不会对处理易失性变量的代码应用某些优化——例如,它不会在不将其写入内存的情况下将其加载到寄存器。这在处理硬件寄存器时很重要。

Volatile也很有用,当你想强制编译器不优化特定的代码序列时(例如编写一个微基准测试)。

volatile的另一个用途是信号处理程序。如果你有这样的代码:

int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}

编译器可以注意到循环体没有触及quit变量,并将循环转换为while (true)循环。即使在SIGINTSIGTERM的信号处理程序上设置了quit变量;编译器无法知道这一点。

然而,如果quit变量被声明为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的内存位置,从而丧失最终的优化机会。

参见Andrei Alexandrescu的文章,“volatile——多线程程序员最好的朋友

挥发性关键字was 设计用于防止编译器 可能呈现代码的优化 在某些人面前是不正确的 异步事件。例如,如果 将一个基本变量声明为 挥发性,编译器不是 允许在寄存器中缓存它 一个常见的优化就是 如果这个变量是灾难性的 在多个线程之间共享。因此, 一般的规则是,如果你有变量 必须共享的原始类型 在多个线程中,声明它们 挥发性变量。但是你可以 用这个做更多的事情 关键字:你可以用它来抓代码 这不是线程安全的,但是可以 在编译时这样做。这篇文章 展示了它是如何完成的;解决方案 涉及到一个简单的智能指针 也使它易于序列化

.代码的关键部分

本文适用于CC++

另见Scott Meyers和Andrei Alexandrescu的文章“c++和双重检查锁定的危险”:

所以当处理一些内存位置(例如内存映射端口或ISRs引用的内存[中断服务例程])时,一些优化必须挂起。Volatile的存在是为了指定对这些位置的特殊处理,特别是:(1)Volatile变量的内容是“不稳定的”(可以通过编译器未知的方式改变),(2)所有对Volatile数据的写入都是“可观察的”,因此必须严格执行,(3)所有对Volatile数据的操作都按照它们在源代码中出现的顺序执行。前两条规则保证了正确的阅读和写作。最后一种允许实现混合输入和输出的I/O协议。这是非正式的C和c++的volatile保证。

Volatile意味着存储可能在任何时候被改变,而且是在用户程序控制之外被改变。这意味着如果你引用变量,程序应该总是检查物理地址(即映射的输入fifo),而不是以缓存的方式使用它。

它有两个用途。这些在嵌入式开发中特别常用。

  1. 编译器不会优化使用volatile关键字定义的变量的函数

  2. Volatile用于访问RAM、ROM等中的精确内存位置。这通常用于控制内存映射设备,访问CPU寄存器和定位特定的内存位置。

参见程序集列表示例。 回复:在嵌入式开发中使用C“volatile”关键字 < / p >

我会提到另一个挥发物很重要的场景。

假设您为更快的I/O对文件进行内存映射,并且该文件可以在幕后更改(例如,该文件不在您的本地硬盘驱动器上,而是通过网络由另一台计算机提供)。

如果您通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,那么编译器生成的代码可以多次获取相同的数据,而您却不知道它。

如果该数据碰巧发生了变化,您的程序可能会使用两个或多个不同版本的数据,并进入不一致的状态。如果程序处理不受信任的文件或来自不受信任位置的文件,这不仅会导致程序的逻辑不正确行为,而且还会导致可利用的安全漏洞。

如果您关心安全性,这是一个需要考虑的重要场景。

维基上有关于volatile的一切:

Linux内核的文档也对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循环中它将永远为0。优化后,编译器会一直将其视为while(true),导致无限循环。

为了避免这种情况,我们将标志声明为volatile,我们告诉编译器这个值可能会被外部接口或程序的其他模块改变,也就是说,请不要优化它。这就是volatile的用例。

在我看来,你不应该对volatile期望太高。为了说明,请看Nils Pipenbrinck的高票回答中的例子。

我想说,他的例子不适合volatilevolatile只用于: 阻止编译器进行有用和理想的优化。这与线程安全、原子访问甚至内存顺序无关。

在这个例子中:

    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之前,编译器仅在编译后的代码中保证。在运行时,处理器仍然可能根据处理器架构对数据和命令分配进行重新排序。硬件可能会得到错误的数据(假设gadget映射到硬件I/O)。数据和命令分配之间需要内存屏障。

在Dennis Ritchie设计的语言中,除了地址未被获取的自动对象外,对任何对象的每次访问都表现为计算对象的地址,然后在该地址上读写存储。这使得该语言非常强大,但严重限制了优化机会。

虽然可以添加一个限定符,让编译器假定某个特定对象不会以奇怪的方式被更改,但这样的假设适用于C程序中的绝大多数对象,并且对所有适用这种假设的对象添加限定符是不切实际的。另一方面,有些程序需要使用一些对象,而这种假设并不成立。为了解决这个问题,标准说编译器可以假设没有声明volatile的对象的值不会被观察到或以编译器控制之外的方式改变,或者在合理的编译器理解之外。

因为不同的平台可能有不同的方法,可以在编译器的控制之外观察或修改对象,因此这些平台的高质量编译器在对volatile语义的准确处理上应该有所不同。不幸的是,由于标准未能建议用于平台上低级编程的高质量编译器应该以一种能够识别该平台上特定读/写操作的任何和所有相关影响的方式来处理volatile,许多编译器在处理后台I/O等事情方面做得很差,而这种方式是有效的,但不会被编译器“优化”破坏。

简单来说,它告诉编译器不要对特定变量做任何优化。映射到设备寄存器的变量由设备间接修改。在这种情况下,必须使用volatile。

正如这里许多人正确地建议的那样,volatile关键字的流行用途是跳过volatile变量的优化。

在阅读了volatile之后,我想到的最好的优点,并且值得一提的是——在longjmp的情况下,防止变量的回滚。非本地跳转。

这是什么意思?

它只是意味着在执行堆栈解除之后,最后一个值将被保留,以返回到之前的某个堆栈帧;通常是在一些错误的情况下。

因为它超出了这个问题的范围,所以我不打算在这里详细讨论setjmp/longjmp,但它值得一读;以及如何使用波动特征来保留最后的价值。