通过访问函数访问共享内存是否需要“易失性”?
[编辑] 对于背景阅读,并且要明确的是,这就是我所说的:易失性关键字简介
在检查嵌入式系统代码时,我看到的最常见错误之一是遗漏了线程/中断共享数据的易失性。然而我的问题是,当通过访问函数或成员函数访问变量时,不使用 volatile 是否“安全”?
一个简单的例子;在下面的代码中......
volatile bool flag = false ;
void ThreadA()
{
...
while (!flag)
{
// Wait
}
...
}
interrupt void InterruptB()
{
flag = true ;
}
变量 flag
必须是易失性的,以确保 ThreadA 中的读取不会被优化,但是如果通过函数读取该标志
volatile bool flag = false ;
bool ReadFlag() { return flag }
void ThreadA()
{
...
while ( !ReadFlag() )
{
// Wait
}
...
}
...... .flag
还需要是易失性的吗?我意识到它是不稳定的并没有什么坏处,但我担心的是它何时被省略并且省略未被发现;这安全吗?
上面的例子很简单;在真实情况下(也是我提出要求的原因),我有一个包装 RTOS 的类库,这样就有一个派生任务对象的抽象类 cTask 。这种“活动”对象通常具有访问数据的成员函数,这些数据可以在对象的任务上下文中修改,但可以从其他上下文访问;那么将此类数据声明为易失性是否至关重要?
我真正感兴趣的是这些数据的保证,而不是实际的编译器可能会做什么。我可能测试了许多编译器,发现它们从未优化通过访问器的读取,但有一天发现一个编译器或编译器设置使这一假设不成立。例如,我可以想象,如果函数是内联的,这样的优化对于编译器来说将是微不足道的,因为它与直接读取没有什么不同。
[edit] For background reading, and to be clear, this is what I am talking about: Introduction to the volatile keyword
When reviewing embedded systems code, one of the most common errors I see is the omission of volatile for thread/interrupt shared data. However my question is whether it is 'safe' not to use volatile
when a variable is accessed via an access function or member function?
A simple example; in the following code...
volatile bool flag = false ;
void ThreadA()
{
...
while (!flag)
{
// Wait
}
...
}
interrupt void InterruptB()
{
flag = true ;
}
... the variable flag
must be volatile to ensure that the read in ThreadA is not optimised out, however if the flag were read via a function thus...
volatile bool flag = false ;
bool ReadFlag() { return flag }
void ThreadA()
{
...
while ( !ReadFlag() )
{
// Wait
}
...
}
... does flag
still need to be volatile? I realise that there is no harm in it being volatile, but my concern is for when it is omitted and the omission is not spotted; will this be safe?
The above example is trivial; in the real case (and the reason for my asking), I have a class library that wraps an RTOS such that there is an abstract class cTask that task objects are derived from. Such "active" objects typically have member functions that access data than may be modified in the object's task context but accessed from other contexts; is it critical then that such data is declared volatile?
I am really interested in what is guaranteed about such data rather than what a practical compiler might do. I may test a number of compilers and find that they never optimise out a read through an accessor, but then one day find a compiler or a compiler setting that makes this assumption untrue. I could imagine for example that if the function were in-lined, such an optimisation would be trivial for a compiler because it would be no different than a direct read.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我对 C99 的理解是,除非您指定 volatile,否则实际访问变量的方式和时间是实现定义的。如果您指定
易失性
限定符,则代码必须根据抽象机器的规则工作。标准中的相关部分是:
6.7.3 类型限定符
(易失性描述)和5.1.2.3 程序执行
(抽象机器定义)。一段时间以来,我知道许多编译器实际上都有启发式方法来检测何时应该再次重新读取变量以及何时可以使用缓存副本。 Volatile 向编译器明确表明,对变量的每次访问实际上都应该是对内存的访问。如果没有 易失性,编译器似乎可以自由地永远不会重新读取该变量。
顺便说一句,将访问包装在函数中并不会改变这一点,因为即使没有内联的函数也可能仍被当前编译单元内的编译器内联。
PS 对于 C++ 可能值得检查前者所基于的 C89。我手头没有C89。
My reading of C99 is that unless you specify
volatile
, how and when the variable is actually accessed is implementation defined. If you specifyvolatile
qualifier then code must work according to the rules of an abstract machine.Relevant parts in the standard are:
6.7.3 Type qualifiers
(volatile description) and5.1.2.3 Program execution
(the abstract machine definition).For some time now I know that many compilers actually have heuristics to detect cases when a variable should be reread again and when it is okay to use a cached copy. Volatile makes it clear to the compiler that every access to the variable should be actually an access to the memory. Without volatile it seems compiler is free to never reread the variable.
And BTW wrapping the access in a function doesn't change that since a function even without
inline
might be still inlined by the compiler within the current compilation unit.P.S. For C++ probably it is worth checking the C89 which the former is based on. I do not have the C89 at hand.
是的,这很关键。
就像您所说的,
易失性
可防止共享内存上的代码破坏优化[C++98 7.1.5p8]
。由于您永远不知道给定编译器现在或将来可能会执行哪种优化,因此您应该明确指定变量是易失性的。
Yes it is critical.
Like you said
volatile
prevents code breaking optimization on shared memory[C++98 7.1.5p8]
.Since you never know what kind of optimization a given compiler may do now or in the future, you should explicitly specify that your variable is volatile.
当然,在第二个示例中,省略了写入/修改变量“flag”。如果它从未被写入,则不需要它是易失性的。
关于主要问题
即使每个线程都通过相同的函数访问/修改该变量,该变量仍然需要被标记为易失性。
一个函数可以在多个线程中同时“活动”。想象一下,函数代码只是一个由线程获取并执行的蓝图。如果线程B 中断线程A 中ReadFlag 的执行,它只是执行ReadFlag 的不同副本(具有不同的上下文,例如不同的堆栈、不同的寄存器内容)。这样做可能会扰乱线程 A 中 ReadFlag 的执行。
Of course, in the second example, writing/modifying variable 'flag' is omitted. If it is never written to, there is no need for it being volatile.
Concerning the main question
The variable has still to be flagged volatile even if every thread accesses/modifies it through the same function.
A function can be "active" simultaneously in several threads. Imagine that the function code is just a blueprint that gets taken by a thread and executed. If thread B interrupts the execution of ReadFlag in thread A, it simply executes a different copy of ReadFlag (with a different context, e.g. a different stack, different register contents). And by doing so, it could mess up the execution of ReadFlag in thread A.
在 C 语言中,这里不需要
volatile
关键字(一般意义上)。来自 ANSI C 规范 (C89),A8.2 节“类型说明符”:
Kernighan 和 Ritchie对此部分的评论(参考
const< /code> 和
易失性
说明符):鉴于这些详细信息,您无法保证特定编译器如何解释
volatile
关键字,或者它是否完全忽略它。在任何情况下都不应该将完全依赖于实现的关键字视为“必需”。话虽如此,K&R 还指出:
在实践中,这就是我所见过的每个编译器如何解释
易失性
的方式。将变量声明为易失性
,编译器不会尝试以任何方式优化对其的访问。大多数时候,现代编译器能够很好地判断变量是否可以安全地缓存。如果您发现您的特定编译器正在优化一些不应该优化的内容,那么添加
volatile
关键字可能是合适的。但请注意,这可能会限制编译器对使用 volatile 变量的函数中的其余代码执行的优化量。有些编译器在这方面比其他编译器做得更好;我使用的一个嵌入式 C 编译器会关闭对访问易失性
的函数的所有优化,但 gcc 等其他编译器似乎仍然能够执行一些有限的优化。通过访问器函数访问变量应该防止函数缓存该值。即使函数是自动内联的,每次调用该函数都应该重新调用该函数并重新获取新值。我从未见过编译器会自动内联访问器函数,然后优化数据重新获取。我并不是说它不会发生(因为这是依赖于实现的行为),但我不会编写任何期望这种情况发生的代码。您的第二个示例本质上是在变量周围放置一个包装器 API,并且库在不始终使用
易失性
的情况下执行此操作。总而言之,C 中对易失性对象的处理取决于实现。根据 ANSI C89 规范,它们没有任何“保证”。
您的代码在线程和中断例程之间共享 易失性 对象。没有编译器实现(我见过)能够提供足够的功能来处理并行访问。您应该使用某种锁定机制来保证两个线程(在你的第一个例子)不要踩到对方的脚趾(即使一个是中断处理程序,你仍然可以在多CPU或多核系统上进行并行访问)。
In C, the
volatile
keyword is not required here (in the general sense).From the ANSI C spec (C89), section A8.2 "Type Specifiers":
Kernighan and Ritchie comment on this section (referring to the
const
andvolatile
specifiers):Given these details, you can't be guaranteed how a particular compiler interprets the
volatile
keyword, or if it ignores it altogether. A keyword that is completely implementation dependent shouldn't be considered "required" in any situation.That being said, K&R also state that:
In practice, this is how practically every compiler I have seen interprets
volatile
. Declare a variable asvolatile
and the compiler will not attempt to optimize accesses to it in any way.Most of the time, modern compilers are pretty good about judging whether or not a variable can be safely cached or not. If you find that your particular compiler is optimizing away something that it shouldn't, then adding a
volatile
keyword might be appropriate. Be aware, though, that this can limit the amount of optimization that the compiler can do on the rest of the code in the function that uses thevolatile
variable. Some compilers are better about this than others; one embedded C compiler I used would turn off all optimizations for a function that accesses avolatile
, but others like gcc seem to be able to still perform some limited optimizations.Accessing the variable through an accessor function should prevent the function from caching the value. Even if the function is auto-inlined, each call to the function should re-call the function and re-fetch a new value. I have never seen a compiler that would auto-inline the accessor function and then optimize away the data re-fetch. I'm not saying it can't happen (since this is implementation-dependent behavior), but I wouldn't write any code that expects that to happen. Your second example is essentially placing a wrapper API around the variable, and libraries do this without using
volatile
all the time.All in all, the treatment of
volatile
objects in C is implementation-dependent. There is nothing "guaranteed" about them according to the ANSI C89 spec.Your code is sharing the
volatile
object between a thread and an interrupt routine. No compiler implementation (that I have ever seen) givesvolatile
enough power to be sufficient for handling parallel access. You should use some sort of locking mechanism to guarantee that the two threads (in your first example) don't step on each other's toes (even though one is an interrupt handler, you can still have parallel access on a multi-CPU or multi-core system).编辑:我没有仔细阅读代码,所以我认为这是一个关于线程同步的问题,永远不应该使用
volatile
,但是这种用法看起来可能没问题(取决于还如何使用有问题的变量,以及中断是否始终运行,使其内存视图与线程看到的内存视图(缓存)一致。 /code> 限定符,如果你将它包装在函数调用中?’接受的答案是正确的,你不能。我将保留我原来的答案,因为对于阅读这个问题的人来说,知道易失性 在某些非常特殊的情况之外几乎没有用。
更多编辑:您的 RTOS 用例可能需要除易失性之外的额外保护,在某些情况下您可能需要使用内存屏障或使它们成为原子......我真的无法告诉当然,这只是你需要小心的事情(我建议查看下面的 Linux 内核文档链接,Linux 不会使用
volatile
来处理此类事情,非常可能有充分的理由)。何时需要或不需要volatile
在很大程度上取决于您所运行的 CPU 的内存模型,并且通常volatile
还不够好。volatile
是执行此操作的错误方法,它不能保证此代码能够工作,它不适合这种用途。易失性用于读取/写入内存映射设备寄存器,因此它足以达到此目的,但是当您谈论线程之间的内容时它没有帮助。 (特别是编译器仍然大声地重新排序一些读取和写入,就像CPU在执行时一样(这非常重要,因为
易失性
不会告诉CPU做任何特殊的事情(有时这意味着绕过缓存,但这取决于编译器/CPU))请参阅 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html,英特尔开发人员文章,CERT, Linux 内核文档
这些文章的简短版本,
volatile< /code> 按照您想要的方式使用既不好又错误。不好是因为它会让你的代码变慢,错误是因为它实际上并没有做你想做的事情。
实际上,在 x86 上,无论有或没有
易失性
,您的代码都可以正常运行,但是它是不可移植的。编辑:请注意自己实际上阅读了代码...这是 易失性 要做的事情。
Edit: I didn't read the code very closely and so I thought this was a question about thread synchronization, for which
volatile
should never be used, however this usage looks like it might be OK (Depending on how else the variable in question is used, and if the interrupt is always running such that it's view of memory is (cache-)coherent with the one that the thread sees. In the case of 'can you remove thevolatile
qualifier if you wrap it in a function call?' the accepted answer is correct, you cannot. I'm going to leave my original answer because it's important for people reading this question to know thatvolatile
is almost useless outside of certain very special cases.More Edit: Your RTOS use case may require additional protection above and beyond volatile, you may need to use memory barriers in some cases or make them atomic... I can't really tell you for sure, it's just something you need to be careful of (I'd suggest looking at the Linux kernel documentation link I have below though, Linux doesn't use
volatile
for that kind of thing, very probably with a good reason). Part of when you do and do not needvolatile
depends very strongly on the memory model of the CPU you're running on, and oftenvolatile
is not good enough.volatile
is the WRONG way to do this, it does NOT guarantee that this code will work, it wasn't meant for this kind of use.volatile
was intended for reading/writing to memory mapped device registers, and as such it is sufficient for that purpose, however it DOES NOT help when you're talking about stuff going between threads. (In particular the compiler is still aloud to re-order some reads and writes, as is the CPU while it's executing (this one's REALLY important sincevolatile
doesn't tell the CPU to do anything special (sometimes it means bypass cache, but that's compiler/CPU dependent))see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html, Intel developer article, CERT, Linux kernel documentation
Short version of those articles,
volatile
used the way you want to is both BAD and WRONG. Bad because it will make your code slower, wrong because it doesn't actually do what you want.In practice, on x86 your code will function correctly with or without
volatile
, however it will be non-portable.EDIT: Note to self actually read the code... this is the sort of thing
volatile
is meant to do.