c - volatile 及其有害影响

标签 c linux-kernel embedded embedded-linux

我是一名嵌入式开发人员,在使用 I/O 端口时使用 volatile 关键字。但是我的项目经理建议使用 volatile 关键字是有害的并且有很多缺点,但是我发现在大多数情况下 volatile 在嵌入式编程中很有用,据我所知 volatile 在内核代码中是有害的,因为对我们的代码的更改将变得不可预测的。在嵌入式系统中使用 volatile 也有什么缺点吗?

最佳答案

不,volatile无害。在任何情况下。曾经。添加 volatile 后,没有可能的格式良好的代码会中断。到一个对象(以及指向该对象的指针)。然而, volatile往往知之甚少 .内核文档说明 volatile 的原因被认为是有害的,因为人们一直使用它以破坏的方式在内核线程之间进行同步。特别是,他们使用了 volatile整数变量就好像对它们的访问保证是原子的,但事实并非如此。
volatile也不是没用,特别是如果你去裸机,你会需要它。但是,与任何其他工具一样,理解 volatile 的语义很重要。在使用它之前。

什么 volatile

访问 volatile在标准中,对象被认为是副作用,就像增加或减少 ++ 一样。和 -- .特别是,这意味着 5.1.2.3 (3),它说

(...) An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object)



不适用。 编译器必须剔除它认为它知道的关于 volatile 的值的所有内容。每个序列点的变量。 (与其他副作用一样,当访问 volatile 对象发生时,由序列点控制)

这样做的效果主要是禁止某些优化。以代码为例
int i;

void foo(void) {
  i = 0;
  while(i == 0) {
    // do stuff that does not touch i
  }
}

允许编译器将其变成从不检查 i 的无限循环。又是因为可以推导出i的值在循环中没有改变,因此 i == 0永远不会是假的。 即使有另一个线程或中断处理程序可能会改变 i 也是如此。 .编译器不知道它们,它也不关心。明确允许不关心。

将此与
int volatile i;

void foo(void) {
  i = 0;
  while(i == 0) { // Note: This is still broken, only a little less so.
    // do stuff that does not touch i
  }
}

现在编译器必须假设 i可以随时更改,不能做这个优化。这当然意味着,如果您处理中断处理程序和线程,volatile对象是同步所必需的。 然而,它们还不够。

什么 volatile不是

什么 volatile不保证是原子访问。如果您习惯于嵌入式编程,这应该很直观。如果您愿意,请考虑以下用于 8 位 AVR MCU 的代码:
uint32_t volatile i;

ISR(TIMER0_OVF_vect) {
  ++i;
}

void some_function_in_the_main_loop(void) {
  for(;;) {
    do_something_with(i); // This is thoroughly broken.
  }
}

这段代码被破坏的原因是访问 i不是原子的——在 8 位 MCU 上不能是原子的。例如,在这个简单的情况下,可能会发生以下情况:
  • i0x0000ffff
  • do_something_with(i)即将被叫
  • i的高两个字节被复制到此调用的参数槽中
  • 此时定时器0溢出,主循环中断
  • ISR 更改 i . i的低两个字节溢出,现在是 0 . i现在是 0x00010000 .
  • 主循环继续,i的低两个字节被复制到参数槽
  • do_something_with0 调用作为其参数。

  • 类似的事情可能发生在 PC 和其他平台上。如果有的话,更复杂的架构可能会带来更多失败的机会。

    带走

    所以不,使用 volatile还不错,您将(通常)必须在裸机代码中执行此操作。然而,当你使用它时,你必须记住它不是魔杖,你仍然需要确保你不会被自己绊倒。在嵌入式代码中,通常有一种特定于平台的方法来处理原子性问题;例如,在 AVR 的情况下,通常的 Crowbar 方法是在持续时间内禁用中断,如
    uint32_t x;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      x = i;
    }
    do_something_with(x);
    

    ...哪里ATOMIC_BLOCK宏调用 cli() (禁用中断)之前和 sei() (启用中断)之后,如果它们事先被启用。

    C11 是第一个明确承认多线程存在的 C 标准,引入了一系列新的原子类型和内存防护操作,可用于线程间同步,并在许多情况下使用 volatile不必要。如果您可以使用它们,请使用它们,但它们可能需要一段时间才能到达所有常见的嵌入式工具链。有了它们,上面的循环可以像这样修复:
    atomic_int i;
    
    void foo(void) {
      atomic_store(&i, 0);
      while(atomic_load(&i) == 0) {
        // do stuff that does not touch i
      }
    }
    

    ...以其最基本的形式。更宽松的内存顺序语义的精确语义超出了 SO 答案的范围,因此我将在这里坚持使用默认的顺序一致的内容。

    如果您对此感兴趣,Gil Hamilton 在评论中提供了一个链接,用于解释使用 C11 原子的无锁堆栈实现,尽管我不认为这是对内存顺序语义本身的非常好的描述。然而,C11 模型似乎与 C++11 内存模型非常相似,其中存在一个有用的介绍 here .如果我找到 C11 特定文章的链接,我会稍后放在这里。

    关于c - volatile 及其有害影响,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27777140/

    相关文章:

    c++ - 通过 MonoDevelop 4.0 a.k.a. Xamarin Studio 获得 C/C++ 项目支持

    c - 从一组表达式中评估以达到最高分数的最佳表达式 - Google Interview

    c - 随机 int 1-255 到 C 中的字符

    android - c undefined reference to function - 编译android/linux内核

    c - tcp socket hungry 程序是否会干扰文件系统 I/O

    embedded - 具有多个串行端口的 USB 通信设备,适用于所有平台

    c++ - 在非托管库方法中使用托管 C++ 中的指针

    linux - ARM 上的断点

    assembly - 处理器之间的转换

    嵌入式系统 : last gasp before reboot