21

[编辑]对于背景阅读,并且要清楚,这就是我所说的:volatile关键字简介

在查看嵌入式系统代码时,我看到的最常见错误之一是线程/中断共享数据的 volatile 遗漏。volatile但是我的问题是,当通过访问函数或成员函数访问变量时不使用它是否“安全” ?

一个简单的例子;在下面的代码中......

volatile bool flag = false ;
void ThreadA()
{
    ...
    while (!flag)
    {
        // Wait
    }
    ...
}

interrupt void InterruptB()
{
    flag = true ;
} 

...变量flag必须是 volatile 以确保 ThreadA 中的读取不会被优化,但是如果标志是通过函数读取的...

volatile bool flag = false ;
bool ReadFlag() { return flag }
void ThreadA()
{
    ...
    while ( !ReadFlag() )
    {
        // Wait
    }
    ...
}

......flag仍然需要不稳定吗?我意识到它是 volatile 并没有什么坏处,但我关心的是何时省略它并且没有发现遗漏;这会安全吗?

上面的例子很简单;在实际情况中(以及我询问的原因),我有一个包装 RTOS 的类库,因此有一个抽象类 cTask 派生任务对象。这种“活动”对象通常具有访问数据的成员函数,这些函数可以在对象的任务上下文中修改但从其他上下文访问;那么,这些数据被宣布为易失性至关重要吗?

我真的对这些数据的保证内容感兴趣,而不是实际的编译器可能会做什么。我可能测试了许多编译器,发现它们永远不会优化通过访问器的读取,但有一天会发现一个编译器或编译器设置使这个假设不成立。例如,我可以想象,如果函数是内联的,那么这种优化对于编译器来说将是微不足道的,因为它与直接读取没有什么不同。

4

5 回答 5

12

我对 C99 的解读是,除非您指定volatile,否则实际访问变量的方式和时间由实现定义。如果您指定volatile限定符,则代码必须根据抽象机器的规则工作。

标准中的相关部分是:(6.7.3 Type qualifiers易失性描述)和5.1.2.3 Program execution(抽象机器定义)。

一段时间以来,我知道许多编译器实际上具有启发式方法来检测何时应重新读取变量以及何时可以使用缓存副本的情况。Volatile 让编译器清楚地知道,对变量的每次访问实际上都应该是对内存的访问。如果没有 volatile,编译器似乎可以自由地从不重新读取变量。

顺便说一句,将访问包装在一个函数中并不会改变这一点,因为即使没有函数inline,编译器仍可能在当前编译单元中内联该函数。

PS 对于 C++,可能值得检查前者所基于的 C89。我手头没有C89。

于 2010-06-30T11:31:35.997 回答
5

是的,这很关键。
就像你说volatile的防止共享内存上的代码破坏优化[C++98 7.1.5p8]
由于您永远不知道给定编译器现在或将来可能会进行哪种优化,因此您应该明确指定您的变量是易失的。

于 2010-06-30T11:30:16.260 回答
1

当然,在第二个例子中,写入/修改变量'flag'被省略了。如果它从未被写入,则不需要它是易失的。

关于主要问题

即使每个线程都通过相同的函数访问/修改它,该变量仍必须标记为 volatile。

一个函数可以在多个线程中同时“激活”。想象一下,函数代码只是一个由线程获取并执行的蓝图。如果线程 B 中断线程 A 中 ReadFlag 的执行,它只会执行 ReadFlag 的不同副本(具有不同的上下文,例如不同的堆栈,不同的寄存器内容)。这样做可能会弄乱线程 A 中 ReadFlag 的执行。

于 2010-06-30T11:17:02.703 回答
1

在 C 中,volatile此处不需要关键字(一般意义上)。

来自 ANSI C 规范 (C89),第 A8.2 节“类型说明符”:

对象没有独立于实现的语义volatile

Kernighan 和 Ritchie评论本节(指constvolatile说明符):

除了应该诊断更改const 对象的显式尝试之外,编译器可能会忽略这些限定符。

鉴于这些细节,您无法保证特定编译器如何解释volatile关键字,或者它是否完全忽略它。在任何情况下都不应将完全依赖于实现的关键字视为“必需”。

话虽如此,K&R 还声明:

的目的volatile是强制实现抑制可能发生的优化。

在实践中,这就是我所见过的每个编译器实际上是如何解释volatile. 将变量声明为volatile,编译器将不会尝试以任何方式优化对它的访问。

大多数时候,现代编译器非常擅长判断变量是否可以安全缓存。如果您发现您的特定编译器正在优化它不应该的东西,那么添加volatile关键字可能是合适的。但是请注意,这可能会限制编译器可以对使用该volatile变量的函数中的其余代码进行的优化量。一些编译器在这方面比其他编译器更好;我使用的一个嵌入式 C 编译器会关闭访问 a 的函数的所有优化volatile,但是像 gcc 这样的其他编译器似乎仍然能够执行一些有限的优化。

通过访问器函数访问变量应该防止函数缓存值。即使函数是自动内联的,每次调用函数都应该重新调用函数并重新获取一个新值。我从未见过会自动内联访问器函数然后优化数据重新获取的编译器。我并不是说它不会发生(因为这是依赖于实现的行为),但我不会编写任何期望发生这种情况的代码。您的第二个示例本质上是在变量周围放置一个包装 API,而库在不使用volatile所有时间的情况下执行此操作。

总而言之,volatileC 中对对象的处理是依赖于实现的。根据 ANSI C89 规范,它们没有任何“保证”。

您的代码volatile在线程和中断例程之间共享对象。没有编译器实现(我见过)提供volatile足够的能力来处理并行访问。您应该使用某种锁定机制来保证两个线程(在您的第一个示例中)不会踩到对方的脚趾(即使一个是中断处理程序,您仍然可以在多 CPU 或多-核心系统)。

于 2010-06-30T16:18:21.927 回答
0

编辑:我没有仔细阅读代码,所以我认为这是一个关于线程同步的问题,volatile不应该使用它,但是这种用法看起来可能没问题(取决于如何使用相关变量,并且如果中断始终在运行,以至于它的内存视图与线程看到的内存视图是(缓存)一致的。在“volatile如果你将它包装在函数调用中,你能删除限定符吗?”的情况下,接受答案是正确的,你不能。我将留下我原来的答案,因为对于阅读这个问题的人来说,知道这volatile在某些非常特殊的情况之外几乎没有用是很重要的。

更多编辑:您的 RTOS 用例可能需要超出易失性的额外保护,在某些情况下您可能需要使用内存屏障或使它们成为原子......我不能确定地告诉你,这只是你需要的东西小心(我建议查看下面的 Linux 内核文档链接,Linux 不volatile用于那种事情,很可能是有充分理由的)。您需要和不需要的部分时间volatile很大程度上取决于您正在运行的 CPU 的内存模型,而且通常volatile还不够好。

volatile这样做的方法是错误的,它不能保证这段代码可以工作,它不适合这种用途。

volatile旨在读取/写入内存映射设备寄存器,因此足以达到此目的,但是当您谈论线程之间的内容时它没有帮助。(特别是编译器仍然大声重新排序一些读取和写入,就像 CPU 在执行时一样(这非常重要,因为volatile它不会告诉 CPU 做任何特殊的事情(有时这意味着绕过缓存,但那是编译器/CPU 依赖))

请参阅http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html英特尔开发人员文章CERTLinux 内核文档

这些文章的简短版本,volatile以您想要的方式使用,既不好错误。不好是因为它会使你的代码变慢,错误是因为它实际上并没有做你想做的事。

实际上,在 x86 上,无论有没有 ,您的代码都可以正常运行volatile但是它是不可移植的。

编辑:注意自己实际阅读了代码......这就是要做的事情volatile

于 2010-06-30T14:49:36.763 回答