为什么C需要挥发性?

时间:2008-10-29 08:36:39

标签: c declaration volatile

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

18 个答案:

答案 0 :(得分:367)

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

答案 1 :(得分:171)

C中的

volatile实际上是为了不自动缓存变量的值而存在的。它会告诉机器不要缓存此变量的值。因此,每次遇到它时,它将从主存储器中获取给定volatile变量的值。使用此机制是因为OS可以在任何时候修改该值或任何中断。因此,使用volatile将帮助我们每次重新访问该值。

答案 2 :(得分:165)

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

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

允许编译器注意到循环体不接触quit变量并将循环转换为while (true)循环。即使在quitSIGINT的信号处理程序上设置了SIGTERM变量;编译器无法知道这一点。

但是,如果quit变量声明为volatile,则每次都强制编译器加载它,因为它可以在其他地方修改。在这种情况下,这正是您想要的。

答案 3 :(得分:56)

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

答案 4 :(得分:30)

见Andrei Alexandrescu撰写的这篇文章,“volatile - Multithreaded Programmer's Best Friend

  

volatile 关键字是   设计为防止编译   可能呈现代码的优化   在某些情况下是不正确的   异步事件。例如,如果   你将一个原始变量声明为    volatile ,编译器不是   允许将其缓存在注册表中 -   一种常见的优化方式   如果那个变量是灾难性的   在多个线程之间共享。所以   一般规则是,如果你有变量   必须共享的原始类型   在多个线程中,声明那些   变量 volatile 。但是你可以   实际上用这个做了很多   keyword:您可以使用它来捕获代码   这不是线程安全的,你可以   在编译时这样做。本文   展示了它是如何完成的;解决方案   涉及一个简单的智能指针   也使序列化变得容易   代码的关键部分。

该文章适用于CC++

另请参阅Scott Meyers和Andrei Alexandrescu撰写的文章“C++ and the Perils of Double-Checked Locking”:

  

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

答案 5 :(得分:27)

我的简单解释是:

在某些情况下,基于逻辑或代码,编译器将对其认为不会更改的变量进行优化。 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的用例。

答案 6 :(得分:19)

对于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的内存位置,从而丧失最终的优化机会。

答案 7 :(得分:11)

有两种用途。这些在嵌入式开发中经常被特别使用。

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

  2. Volatile用于访问RAM,ROM等中的确切内存位置......这种方法更常用于控制内存映射设备,访问CPU寄存器和查找特定内存位置。

  3. 查看汇编列表的示例。 Re: Usage of C "volatile" Keyword in Embedded Development

答案 8 :(得分:10)

当您想强制编译器不优化特定的代码序列时(例如,用于编写微基准测试),易失性也很有用。

答案 9 :(得分:10)

我会提到另一种情况,即挥发物很重要。

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

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

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

如果您关心安全性,那么这是一个需要考虑的重要方案。

答案 10 :(得分:7)

volatile意味着存储可能随时发生变化并被更改,但不受用户程序控制之外的影响。这意味着如果你引用变量,程序应该总是检查物理地址(即映射的输入fifo),而不是以缓存的方式使用它。

答案 11 :(得分:5)

维基说明关于volatile的所有内容:

Linux内核的doc也是关于volatile

的优秀表示法

答案 12 :(得分:4)

在我看来,volatile你不应该期待太多。为了说明,请查看Nils Pipenbrinck's highly-voted answer中的示例。

我想说,他的例子不适合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只能由编译器在编译代码中保证。在运行时,处理器仍然可能重新排序关于处理器体系结构的数据和命令分配。硬件可能会获取错误的数据(假设小工具映射到硬件I / O)。数据和命令分配之间需要内存屏障。

答案 13 :(得分:3)

可以从编译代码外部更改volatile(例如,程序可以将volatile变量映射到内存映射寄存器。)编译器不会对处理volatile变量的代码应用某些优化 - 例如,它不会将其加载到寄存器而不将其写入内存。这在处理硬件寄存器时很重要。

答案 14 :(得分:3)

在丹尼斯·里奇(Dennis Ritchie)设计的语言中,对任何对象的每次访问(除了未获取地址的自动对象之外)都将表现为好像计算了对象的地址,然后在该地址读取或写入了存储。这使该语言非常强大,但是优化机会却非常有限。

虽然可以添加一个限定符来邀请编译器假设某个特定对象不会以怪异的方式进行更改,但这种假设适用于C程序中的绝大多数对象,并且在这样的假设适合的所有对象上添加限定符是不切实际的。另一方面,某些程序需要使用某些对象,而这些对象对此假设将不成立。为了解决此问题,该标准表示,编译器可能会假设未声明volatile的对象将不会以其编译器无法控制的方式或合理的编译器无法理解的方式来观察或更改其值。 / p>

由于各种平台在编译器无法控制的范围内观察或修改对象的方式可能不同,因此适合这些平台的高质量编译器应在对volatile语义的准确处理上有所不同。不幸的是,由于该标准未能建议打算在平台上进行低级编程的高质量编译器应以能够识别该平台上特定读/写操作的所有相关影响的方式处理volatile,因此许多编译器无法做到这一点,从而难以以一种高效的方式处理诸如后台I / O之类的事情,但却无法被编译器的“优化”所破坏。

答案 15 :(得分:3)

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

答案 16 :(得分:0)

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

想到的最大优点是,在阅读完volatile后值得一提的是-防止在longjmp情况下回滚变量。非本地跳转。

这是什么意思?

这仅表示在执行 stack unwinding 后将保留最后一个值,以返回到先前的某个堆栈帧;通常在某些错误情况下。

由于它不在本问题的讨论范围之内,所以我在这里不讨论setjmp/longjmp的细节,但是值得一读。以及如何使用波动率特征来保留最后一个值。

答案 17 :(得分:-2)

它不允许编译器自动更改变量值。 volatile变量用于动态使用。