嵌入式C - 使用"volatile"来断言一致性

bav*_*aza 8 c embedded volatile

请考虑以下代码:

// In the interrupt handler file:
volatile uint32_t gSampleIndex = 0; // declared 'extern'
void HandleSomeIrq()
{
    gSampleIndex++;
}

// In some other file
void Process()
{
    uint32_t localSampleIndex = gSampleIndex;   // will this be optimized away?
    PrevSample      = RawSamples[(localSampleIndex + 0) % NUM_RAW_SAMPLE_BUFFERS];
    CurrentSample   = RawSamples[(localSampleIndex + 1) % NUM_RAW_SAMPLE_BUFFERS];
    NextSample      = RawSamples[(localSampleIndex + 2) % NUM_RAW_SAMPLE_BUFFERS];
}
Run Code Online (Sandbox Code Playgroud)

我的本意是PrevSample,CurrentSampleNextSample是一致的,即使gSampleIndex调用期间被更新Process().

分配到这个localSampleIndex诀窍,还是有可能它会被优化掉,即使它gSampleIndex是不稳定的?

jch*_*jch 8

原则上,仅仅volatile保证Process只能看到一致的值gSampleIndex.但是,实际上,如果uinit32_t硬件直接支持,则不应遇到任何问题.适当的解决方案是使用原子访问.

问题

假设您运行的是16位架构,那么就是指令

localSampleIndex = gSampleIndex;
Run Code Online (Sandbox Code Playgroud)

被编译成两个指令(加载上半部分,加载下半部分).然后可以在两个指令之间调用中断,并且您将获得旧值的一半与新值的一半相结合.

解决方案

解决方案是gSampleCounter仅使用原子操作进行访问.我知道有三种方法可以做到这一点.

C11原子

在C11中(从GCC 4.9开始支持),您将变量声明为原子:

#include <stdatomic.h>

atomic_uint gSampleIndex;
Run Code Online (Sandbox Code Playgroud)

然后,您只需要使用记录的原子接口访问变量.在IRQ处理程序中:

atomic_fetch_add(&gSampleIndex, 1);
Run Code Online (Sandbox Code Playgroud)

并在Process功能:

localSampleIndex = atomic_load(gSampleIndex);
Run Code Online (Sandbox Code Playgroud)

_explicit除非您试图让程序扩展到大量内核,否则不要理会原子函数的变体.

GCC原子

即使你的编译器还不支持C11,它也可能对原子操作有一些支持.例如,在GCC中,您可以说:

volatile int gSampleIndex;
...
__atomic_add_fetch(&gSampleIndex, 1, __ATOMIC_SEQ_CST);
...
__atomic_load(&gSampleIndex, &localSampleIndex, __ATOMIC_SEQ_CST);
Run Code Online (Sandbox Code Playgroud)

如上所述,除非您尝试实现良好的缩放行为,否则不要担心弱一致性.

自己实现原子操作

由于您没有尝试防止来自多个内核的并发访问,只是使用中断处理程序的竞争条件,因此可以仅使用标准C原语实现一致性协议. Dekker的算法是已知最早的此类协议.


Adr*_*tti 0

在您的函数中,您仅访问volatile变量一次(并且它是volatile该函数中唯一的变量),因此您无需担心编译器可能会执行(并阻止)的代码重组。volatile\xc2\xa75.1.2.3 中针对这些优化的标准规定是:

\n\n
\n

在抽象机中,所有表达式都按照语义指定的方式进行计算。如果实际实现可以推断出其值未被使用并且不会产生所需的副作用(包括由调用函数或访问易失性对象引起的任何副作用),则实际实现不需要评估表达式的一部分。

\n
\n\n

请注意最后一句:“...不会产生所需的副作用(...访问易失性对象)”

\n\n

只是volatile会阻止编译器围绕该代码进行的任何优化。仅举几例:没有指令重新排序尊重其他volatile变量。没有表达式删除,没有缓存,没有跨函数的值传播。

\n\n

顺便说一句,我怀疑任何编译器都可能会破坏您的代码(有或没有volatile)。也许本地堆栈变量将被删除,但值将存储在注册表中(确保它不会重复访问内存位置)。您需要volatile的是价值可见性。

\n\n

编辑

\n\n

我认为需要一些澄清。

\n\n

让我安全地假设您知道自己在做什么(您正在使用中断处理程序,因此这不应该是您的第一个 C 程序):CPU 字与您的变量类型匹配并且内存正确对齐。

\n\n

我还假设你的中断是不可重入的(一些魔法 cli/sti东西或者你的CPU为此使用的任何东西),除非你正在计划一些困难的调试和调整。

\n\n

如果满足这些假设,那么您不需要原子操作。为什么?因为localSampleIndex = gSampleIndex它是原子的(因为它正确对齐,字大小匹配并且它是volatile),并且++gSampleIndex不存在任何竞争条件(HandleSomeIrq当它仍在执行时不会再次调用)。他们错了,毫无用处。

\n\n

人们可能会想:“好吧,我可能不需要原子,但为什么我不能使用它们?即使满足这样的假设,这也是一个*额外*,它将实现相同的目标”不,事实并非如此。Atomic 具有不同的volatile变量语义(并且很少volatile/应该在内存映射 I/O 和信号处理之外使用)。易失性(通常)对于原子性来说是没有用的(除非特定的架构说它是),但它有一个很大的区别:可见性。当您在标准gSampleIndex中更新时HandleSomeIrq,保证该值将立即对所有线程(和设备)可见。使用atomic_uint标准保证它将在合理的时间内可见。

\n\n

简而言之:易失性和原子性不是一回事。原子操作对于并发很有用,易失性对于较低级别的东西(中断、设备)很有用。如果您仍然在想“嘿,他们*正是*我需要的”,请阅读从评论中挑选的一些有用的链接:缓存一致性和关于原子的好读物

\n\n

总结一下:
\n在您的情况下,您可以使用带有锁的原子变量(以具有原子访问和值可见性),但地球上没有人会在中断处理程序中放置锁(除非绝对绝对无疑地需要,并且从您发布的代码不是您的情况)。

\n