首页 文章

为什么C需要挥发性?

提问于
浏览
345

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

17 回答

  • 6

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

  • 3

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

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

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

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

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

  • 149

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

  • 9

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

    设计volatile关键字是为了防止在某些异步事件存在时可能导致代码不正确的编译器优化 . 例如,如果将基本变量声明为volatile,则不允许编译器将其缓存在寄存器中 - 如果该变量在多个线程之间共享,则会导致灾难性的常见优化 . 所以一般规则是,如果你有必须在多个线程之间共享的基本类型的变量,则声明那些变量volatile . 但实际上你可以用这个关键字做更多的事情:你可以使用它来捕获非线程安全的代码,你可以在编译时这样做 . 这篇文章展示了它是如何完成的;该解决方案涉及一个简单的智能指针,也可以轻松序列化关键的代码段 .

    本文适用于 CC++ .

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

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

  • 4

    我的简单解释是:

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

  • 158

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

  • 23

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

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

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

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

  • -1

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

  • 2

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

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

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

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

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

  • 18

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

  • 0

    维基说_111974_的一切:

    Linux内核的doc也是关于 volatile 的优秀符号:

  • 27

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

  • 10

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

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

  • 342

    在Dennis Ritchie设计的语言中,除了没有采用地址的自动对象之外,对每个对象的访问都会像计算对象的地址一样,然后在该地址读取或写入存储 . 这使得语言非常强大,但优化机会极为有限 .

    虽然可能添加一个限定符来邀请编译器假设特定对象不会以奇怪的方式进行更改,但这样的假设适用于C程序中的绝大多数对象,并且它会有将限定符添加到适合这种假设的所有对象是不切实际的 . 另一方面,一些程序需要使用某些对象,这种假设不适用 . 为解决此问题,标准规定编译器可能会假定未声明 volatile 的对象不会以超出编译器理解的方式观察或更改其值 .

    因为各种平台可能有不同的方式可以在外面观察或修改对象编译器的控制,这些平台的质量编译器应该在它们对 volatile 语义的精确处理方面有所不同 . 遗憾的是,由于标准未能表明用于平台上的低级编程的质量编译器应该以能够识别该平台上特定读/写操作的任何和所有相关效果的方式处理 volatile ,因此许多编译器都缺乏这样做的方式使得以一种有效但不能被编译器"optimizations"破坏的方式处理像后台I / O这样的事情变得更加困难 .

  • 3

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

  • 9

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

相关问题