【问题标题】:volatile and non-volatile bitfields易失性和非易失性位域
【发布时间】:2017-03-20 11:39:25
【问题描述】:

我正在为 Cortex-M0 CPU 和 gcc 编写代码。我有以下结构:

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

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

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

ISR 如下所示:

void handleIRQ(void) {
    if (!flags.flag1) {
        flags.flag1 = 1;
        // enable some hw timer
    }
}

主循环如下所示:

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)
  • 我已经更新了答案。在位域之后有一个 int 。所以大小应该是8。
  • 您很可能不想为此使用位域。它们是实现定义行为的雷区。即使这是在某个地方定义的,我也不相信编译器会做正确的事情。
  • @user694733 - 这是一个奇怪的位置。如果它是由标准定义的,那么为什么要信任编译器来实现标准所说的任何其他内容?如果它是定义的实现,如果您不信任它的文档,为什么还要使用它?
  • @StoryTeller 我的措辞很差。应该把最后一句话删掉。我的意思是;即使它是由编译器供应商(非 C 标准)定义并在编译器手册中提到的,它也可能在下一次主要的编译器更新中更改,并且在更新过程中很容易被忽视。跨度>

标签: c arm volatile bit-fields


【解决方案1】:

根据我对 C11 标准的解读,为此使用位域是不合适的——即使它们都被声明为 volatile。以下摘自3.14 Memory location

  1. 内存位置
    要么是标量类型的对象,要么是具有非零宽度的相邻位域的最大序列
  2. 注意 1 两个执行线程可以更新和访问单独的内存位置,而不会相互干扰。

  3. 注意 2 如果同时更新同一结构中的两个非原子位域是不安全的 在它们之间声明的成员也是(非零长度)位域,无论它们的大小如何 中间的位域恰好是。

volatile 也不例外。因此,如果两个执行线程(即主线程和 ISR)如果 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 编译器。

然后,您可以检查编译器的手册,如果它们保证对此类易失性对象的访问也保证是原子的。

【讨论】:

  • 您在标准中的何处找到此文本?
  • 是这样想的。注释不是 ISO 标准中的规范文本!但当然注释是正确的,这是一些奇怪的代码。
  • @AnttiHaapala 注释、脚注和示例不是规范性的。此外,C 未指定将unsigned char 用于位字段。该语言仅允许_Bool 和(有符号/无符号)int。因此,如果编译器完全接受它,不知道“修复”会做什么。
【解决方案2】:

只有一位的 volatile 标志并不是那么有意义 - 它甚至可能是有害的。编译器在实践中可能做的是分配两块内存,可能每块 32 位宽。因为 volatile 标志阻止它在同一分配区域内组合两个位,因为没有可用的位级访问指令。

在主循环中访问 flag2 时,编译器是否会优化对它的访问,使其不会在每次在代码中读取或写入时都被提取或存储到内存中?

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

总体而言,不建议使用位域,因为标准对它们的定义很差。在这种情况下,单个 volatile 位可能会导致分配额外的内存。

相反,您应该这样做:

volatile bool flag1;
bool flag2;

假设这些标志不是硬件寄存器的一部分,在这种情况下,代码从一开始就不正确,它们都应该是易失性的。

【讨论】:

  • 我读到 volatile 不会阻止它们进入相同的内存位置。事实上,我的 GCC 似乎将它们都编译为一个 int(2 位结构的大小为 4)
  • @AnttiHaapala 那太糟糕了。 flag2 的读/写不应导致flag1 所在的内存的读/写。处理这个问题可能不是编译器的责任,但它显然需要修复。例如,用我的示例中的两个布尔值替换位域。
  • 不,volatile 不禁止对其进行额外读取,因为它是单个内存位置 - 要强制它位于单独的内存位置,需要在其间添加一个零宽度成员
  • @AnttiHaapala Volatile 并没有禁止它,但它可能会破坏代码。
  • 也许你把 atomic 和 volatile 混淆了 :)
猜你喜欢
  • 1970-01-01
  • 2018-01-18
  • 1970-01-01
  • 2015-11-07
  • 2023-03-21
  • 2023-02-10
  • 1970-01-01
  • 2019-11-07
  • 1970-01-01
相关资源
最近更新 更多