为什么在C中需要使用volatile?

volatile关键字在C语言中用于指示变量的值可能在编译器不知情的情况下被改变,如I/O操作、多线程环境等场景。它阻止编译器对这类变量进行优化,确保每次访问都会从内存中读取最新值。常见的用途包括处理硬件寄存器、避免编译器优化以及在多线程编程中与共享变量的交互。
摘要由CSDN通过智能技术生成

为什么在C中需要使用volatile ? 这有什么用途? 会做什么?


#1楼

维基说了所有关于volatile

Linux内核的文档也对volatile做出了出色的表述:


#2楼

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


#3楼

C中的volatile实际上是为了不自动缓存变量的值而存在的。 它会告诉编译器不要缓存该变量的值。 因此,每次遇到主变量时,它将生成代码以从主内存中获取给定volatile变量的值。 之所以使用这种机制,是因为该值可以随时通过OS或任何中断进行修改。 因此,使用volatile将有助于我们每次重新获得价值。


#4楼

易失性告诉编译器不要优化与易失性变量有关的任何事情。

至少有三个使用它的常见原因,所有这些情况都涉及变量的值可以更改而无需可见代码采取行动的情况。 当另一个正在运行的线程也使用该变量时; 或存在可能更改变量值的信号处理程序时。

假设您有一小部分硬件映射到某处的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;
    }

#5楼

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


#6楼

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


#7楼

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

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

允许编译器注意循环主体不接触quit变量,并将循环转换为while (true)循环。 即使在信号处理程序上为SIGINTSIGTERM设置了quit变量; 编译器没有办法知道。

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


#8楼

我的简单解释是:

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


#9楼

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


#10楼

以下是挥发物的边际用途。 假设您要计算函数f的数值导数:

double der_f(double x)
{
    static const double h = 1e-3;
    return (f(x + h) - f(x)) / h;
}

问题是由于舍入误差, x+hx通常不等于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的内存位置,从而丧失最终的优化机会。


#11楼

请参阅Andrei Alexandrescu的这篇文章,“ volatile-多线程程序员的最佳朋友

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

本文适用于CC++

另请参阅Scott Meyers和Andrei Alexandrescu的文章“ C ++和双重检查锁定的风险 ”:

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


#12楼

我认为,不要对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->data = data 。 在运行时,关于处理器体系结构,处理器仍可能对数据和命令分配进行重新排序。 硬件可能会获得错误的数据(假设小工具已映射到硬件I / O)。 在数据和命令分配之间需要内存屏障。


#13楼

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

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

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


#14楼

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


#15楼

易失性意味着存储可能随时更改并且可能会更改,但是某些内容超出了用户程序的控制范围。 这意味着,如果您引用该变量,则程序应始终检查物理地址(即映射的输入FIFO),而不是以缓存的方式使用它。


#16楼

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

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

  2. 易失性用于访问RAM,ROM等中的确切内存位置。这通常用于控制内存映射的设备,访问CPU寄存器并定位特定的内存位置。

请参见带有装配清单的示例。 回复:嵌入式开发中C“易失性”关键字的用法


#17楼

我将提到另一种情况,其中挥发物很重要。

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

如果通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,则编译器生成的代码可以多次获取相同的数据,而无需您意识到。

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值