易失性和非易失性位域

11

我正在编写适用于Cortex-M0 CPU和GCC的代码。我有以下结构:

struct {
    volatile unsigned flag1: 1;
    unsigned flag2: 1;

    unsigned foo; // something else accessed in main loop
} flags;

flag1在GPIO中断处理程序和主循环中都会读取和写入。而flag2只在主循环中读取和写入。

中断服务子程序如下:

void handleIRQ(void) {
    if (!flags.flag1) {
        flags.flag1 = 1;
        // enable some hw timer
    }
}
主循环看起来像这样:

The main loop looks like this:

for (;;) {
    // disable IRQ
    if (flags.flag1) {
        // handle IRQ
        flags.flag1 = 0;
        // access (rw) flag2 many times
    }
    // wait for interrupt, enable IRQ
}

当在主循环中访问flag2时,编译器是否会优化对它的访问,以便每次读取或写入代码时都不会从内存中获取或存储它?

我不确定,因为要在ISR中设置flag1,需要加载整个char,设置一个位并将其存回。


sizeof(struct flags)是什么? - Daniel Jour
我已经更新了答案。位域后面有一个int。因此大小应该是8。 - woky
3
你很可能不想使用位域来实现这个功能。它们是一个充满着实现定义行为的雷区。即使在某处已经有了定义,我也不会相信编译器会做正确的事情。 - user694733
2
@user694733 - 这是一个奇怪的立场。如果它由标准定义,那么为什么要相信编译器实现标准所说的其他内容呢?如果它是实现定义的,那么如果您不信任其文档,为什么要使用实现呢? - StoryTeller - Unslander Monica
@StoryTeller 我的措辞不太准确。最好把最后一句话留出来。我的意思是; 即使它是由编译器供应商(而不是C标准)定义并在编译器手册中提到,它也可能在下一个主要编译器更新中更改,并且在更新过程中很容易被忽视。 - user694733
2个回答

11
根据C11标准,即使两个变量都被声明为volatile,将它们声明为位域也是不合适的。以下摘自3.14内存位置的内容:
“内存位置可以是标量类型的对象,或具有非零宽度的相邻位域的最大序列。”
提示1:两个执行线程可以更新并访问不相互干扰的单独内存位置。
提示2:如果在它们之间声明的所有成员也是(非零长度)位域,则在同一结构中同时更新两个非原子位域是不安全的,无论这些中间位域的大小如何。
因此,即使使用volatile,如果ISR将更新一个标志而主程序将更新另一个标志,则以上位域使用将不安全。给出的解决方案是添加一个大小为0的成员来强制将它们放置在不同的内存位置。但再次注意,这意味着两个标志将占用至少一个字节的内存,所以更简单的方法是使用非位域的unsigned charbool
struct {
    volatile bool flag1;
    bool flag2;

    unsigned foo; // something else accessed in main loop
} flags;

现在它们将被放置在不同的内存位置,它们可以进行更新而互不干扰。


然而,对于flag1volatile仍然是严格必要的,因为否则对flag1的更新将在主线程中产生副作用,编译器可以推断它只能将该字段保留在一个寄存器中 - 或根本不需要更新。

但是,需要注意的是,在C11下,即使有volatile的保证可能也不够:5.1.2.3p5

当抽象机的处理被接收到信号中断时,既非无锁原子对象也非类型为volatile sig_atomic_t的对象的值是未指定的,浮点环境的状态也是未指定的。由处理程序修改的任何对象的值,如果它既不是无锁原子对象也不是类型为volatile sig_atomic_t,则在处理程序退出时变为未确定的,如果它被处理程序修改并且没有恢复到其原始状态,则浮点环境的状态也是如此。

因此,如果需要完全兼容,则flag1应该是volatile _Atomic bool类型,甚至可以使用_Atomic位域。然而,这两者都需要C11编译器。

然而,您可以检查您的编译器手册,以确定对这些易失性对象的访问是否也保证是原子的。


你在标准中哪里找到这段文本的? - Lundin
想到了。注释不是ISO标准中的规范文本!但当然,这个注释是正确的,这是一些奇怪的代码。 - Lundin
1
@AnttiHaapala 注意事项、脚注和示例不是规范性的。此外,在 C 中使用 unsigned char 作为位域并没有被指定。该语言只允许 _Bool 和 (signed/unsigned) int。因此,无法确定这个“修复”会产生什么效果,如果编译器根本不接受它的话。 - Lundin

5

仅针对一个比特位的volatile标志并不是很有意义,甚至可能有害。实际上编译器可能会分配两个内存块,每个块可能为32比特宽。由于没有可用的比特级访问指令,因此volatile标志阻止了将两个比特位组合在同一分配区域内。

当在主循环中访问flag2时,编译器是否会优化访问,使得每次读写代码中的flag2时都不必从内存中获取或存储?

这很难说,这取决于有多少数据寄存器可用。反汇编代码并查看。

总的来说,不建议使用位域,因为它们在标准中定义得非常糟糕。而且在这种情况下,单个的volatile比特可能会导致额外的内存被分配。

相反,您应该这样做:

volatile bool flag1;
bool flag2;

假设这些标志不是硬件寄存器的一部分,那么代码从一开始就是错误的,它们应该都是易失性的。

据我所知,volatile并不会阻止它们进入相同的内存位置。实际上,我的GCC将它们编译成一个int(2位结构体的大小为4)。 - Antti Haapala -- Слава Україні
@AnttiHaapala 那就非常糟糕了。读写flag2不应导致读写存储flag1的内存。也许编译器没有处理这个责任,但显然需要修复。例如,可以像我的示例一样用两个布尔值替换位域。 - Lundin
不,volatile并不能禁止对它进行额外的读取,因为它是单一的内存位置 - 如果想强制它在一个单独的内存位置中,则需要在其间添加一个零宽度成员。 - Antti Haapala -- Слава Україні
@AnttiHaapala "volatile"不会禁止该操作,但可能会破坏代码。 - Lundin
也许你把 atomic 和 volatile 搞混了 :) - Antti Haapala -- Слава Україні
我认为@Lundin的意思是,从易失性字段中无意读取可能会导致意外的硬件操作。在我的情况下,“flag1”只是内存位置,但由于它是易失性的,编译器应该预期它可能是硬件寄存器中的位,并且读取它可能会引起副作用。 - woky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接