64 位计算机上的哪些类型在 gnu C 和 gnu C++ 中自然是原子的?-- 意味着它们具有原子读取和原子写入

Gab*_*les 10 c c++ gcc x86-64 atomic

注意:对于这个问题,我不是在谈论 C 或 C++语言标准。相反,我谈论的是针对特定体系结构的 gcc 编译器实现,因为语言标准对原子性的唯一保证是使用_AtomicC11 或更高版本中的类型或std::atomic<>C++11 或更高版本中的类型。另请参阅我在这个问题底部的更新。

在任何体系结构上,某些数据类型可以原子方式读取和写入,而其他数据类型则需要多个时钟周期,并且可能在操作中间被中断,如果跨线程共享该数据,则会导致损坏。

8 位单核 AVR 微控制器(例如:Arduino Uno、Nano 或 Mini 使用的 ATmega328 mcu)上,只有8 位数据类型具有原子读取和写入(使用 gcc 编译器和gnu C 或gnu C++语言)。我在不到 2 天的时间里进行了 25 小时的马拉松式调试,然后在这里写下了这个答案。另请参阅此问题的底部以获取更多信息。以及有关使用 AVR-libc 库的 gcc 编译器编译时 AVR 8 位微控制器具有自然原子写入和自然原子读取的 8 位变量的文档。

(32 位)STM32 单核微控制器上,任何32 位或更小的数据类型绝对自动是原子的(当使用 gcc 编译器和 gnu C 或 gnu C++ 语言编译时,因为ISO C 和 C++ 不保证这一点直到 2011 年版本,_Atomic类型为 C11 和std::atomic<>类型为 C++11)。其中包括bool/ _Boolint8_t/ uint8_tint16_t/ uint16_tint32_t/ uint32_tfloat所有指针。唯一的原子类型是int64_t/ uint64_tdouble(8 字节)和long double(也是 8 字节)。我在这里写过:

  1. 哪些变量类型/大小在 STM32 微控制器上是原子的?
  2. 读取由 ISR 更新的 64 位变量
  3. 在 STM32 微控制器中禁用和重新启用中断以实现原子访问防护的各种方法有哪些?

现在我需要知道我的64 位 Linux 计算机。哪些类型绝对自动是原子的?

我的计算机配备 x86-64 处理器和 Linux Ubuntu 操作系统。

我可以使用 Linux 头文件和 gcc 扩展。

我在 gcc 源代码中看到一些有趣的事情,表明至少32 位int类型是原子的。例如:Gnu++ 头文件<bits/atomic_word.h>存储/usr/include/x86_64-linux-gnu/c++/8/bits/atomic_word.h在我的计算机上,并且在线,包含以下内容:

typedef int _Atomic_word;
Run Code Online (Sandbox Code Playgroud)

所以,int显然是原子的。

<bits/types.h>由 包含<ext/atomicity.h>并存储在我的计算机上的Gnu++ 头文件/usr/include/x86_64-linux-gnu/bits/types.h包含以下内容:

/* C99: An integer type that can be accessed as an atomic entity,
   even in the presence of asynchronous interrupts.
   It is not currently necessary for this to be machine-specific.  */
typedef int __sig_atomic_t;
Run Code Online (Sandbox Code Playgroud)

因此,这int显然是原子的。

这是一些示例代码来展示我在说什么......

...当我说我想知道哪些类型具有自然原子读取和自然原子写入,但不是原子递增、递减或复合赋值时。

volatile bool shared_bool;
volatile uint8_t shared u8;
volatile uint16_t shared_u16;
volatile uint32_t shared_u32;
volatile uint64_t shared_u64;
volatile float shared_f; // 32-bits
volatile double shared_d; // 64-bits

// Task (thread) 1
while (true)
{
    // Write to the values in this thread.
    //
    // What I write to each variable will vary. Since other threads are reading
    // these values, I need to ensure my *writes* are atomic, or else I must
    // use a mutex to prevent another thread from reading a variable in the
    // middle of this thread's writing.
    shared_bool = true;
    shared_u8 = 129;
    shared_u16 = 10108;
    shared_u32 = 130890;
    shared_f = 1083.108;
    shared_d = 382.10830;
}

// Task (thread) 2
while (true)
{
    // Read from the values in this thread.
    //
    // What thread 1 writes into these values can change at any time, so I need
    // to ensure my *reads* are atomic, or else I'll need to use a mutex to
    // prevent the other thread from writing to a variable in the midst of
    // reading it in this thread.
    if (shared_bool == whatever)
    {
        // do something
    }
    if (shared_u8 == whatever)
    {
        // do something
    }
    if (shared_u16 == whatever)
    {
        // do something
    }
    if (shared_u32 == whatever)
    {
        // do something
    }
    if (shared_u64 == whatever)
    {
        // do something
    }
    if (shared_f == whatever)
    {
        // do something
    }
    if (shared_d == whatever)
    {
        // do something
    }
}
Run Code Online (Sandbox Code Playgroud)

C_Atomic类型和 C++std::atomic<>类型

我知道C11及更高版本提供_Atomic类型,例如:

const _Atomic int32_t i;
// or (same thing)
const atomic_int_least32_t i;
Run Code Online (Sandbox Code Playgroud)

看这里:

  1. https://en.cppreference.com/w/c/thread
  2. https://en.cppreference.com/w/c/language/atomic

C++11 及更高版本提供了std::atomic<>如下类型:

const std::atomic<int32_t> i;
// or (same thing)
const atomic_int32_t i;
Run Code Online (Sandbox Code Playgroud)

看这里:

  1. https://en.cppreference.com/w/cpp/atomic/atomic

这些 C11 和 C++11“原子”类型提供原子读取和原子写入以及原子递增运算符、递减运算符和复合赋值...

......但这不是我真正要说的。

我想知道哪些类型具有自然原子读取和自然原子写入。对于我所说的,递增、递减和复合赋值不会自然地成为原子的。


2022 年 4 月 14 日更新

我与 ST 的某人进行了一些聊天,似乎 STM32 微控制器只能保证在这些条件下对某些大小的变量进行原子读写:

  1. 你使用汇编。
  2. 您使用 C11_Atomic类型或 C++11std::atomic<>类型。
  3. 您使用带有 gnu 语言和 gcc 扩展的 gcc 编译器。
    1. 我对最后一个最感兴趣,因为这就是我在这个问题顶部的假设的关键,似乎在过去 10 年里一直基于这一点,而我却没有意识到这一点。我希望帮助找到 gcc 编译器手册以及其中解释这些显然存在的原子访问保证的位置。我们应该检查:
      1. 适用于 8 位 AVR ATmega 微控制器的 AVR gcc 编译器手册。
      2. 适用于 32 位 ST 微控制器的 STM32 gcc 编译器手册。
      3. x86-64 gcc 编译器手册??--如果存在这样的东西,对于我的 64 位 Ubuntu 计算机。

到目前为止我的研究:

  1. AVR gcc:不存在 avr gcc 编译器手册。相反,请使用此处的 AVR-libc 手册:https://www.nongnu.org/avr-libc/ -->“用户手册”链接。

    1. 本节中的AVR-libc用户手册<util/atomic> 支持了我的主张,即AVR 上的 8 位类型,当由 gcc 编译时,已经具有自然原子读取自然原子写入,这意味着 8 位读取和写入已经是原子的: (强调):

    需要原子访问的典型示例是在主执行路径和 ISR 之间共享的16(或更多)位变量。

    1. 它谈论的是 C 代码,而不是汇编,因为它在该页面上给出的所有示例都是用 C 语言编写的,包括volatile uint16_t ctr紧随该引用之后的变量示例。

Nat*_*dge 20

从语言标准的角度来看,答案非常简单:它们都不是“绝对自动”原子的

首先,区分“原子”的两种含义很重要。

  • 一是对于信号来说是原子的。例如,这可以确保当您x = 5对 a执行操作时,当前线程中volatile sig_atomic_t调用的信号处理程序将看到旧值或新值。这通常只需在一条指令中进行访问即可完成,因为信号只能由硬件中断触发,而硬件中断只能在指令之间到达。例如, x86 ,即使没有前缀,在这个意义上也是原子的。add dword ptr [var], 12345lock

  • 另一个是关于线程的原子性,因此同时访问该对象的另一个线程将看到正确的值。这更难做到正确。特别是,普通类型的变量对于线程来说不是原子volatile sig_atomic_t你需要_Atomicstd::atomic得到它。

请注意,您的实现为其类型选择的内部名称并不是任何证据。我typedef int _Atomic_word;当然不会推断出“int显然是原子的”;我不知道实现者在什么意义上使用“原子”这个词,或者它是否准确(例如,可以由遗留代码使用)。如果他们想做出这样的承诺,那么它将出现在文档中,而不是出现在应用程序程序员永远不会看到的未解释的标头typedef中。bits


您的硬件可能使某些类型的访问“自动原子化”这一事实并不能告诉您 C/C++ 级别的任何信息。例如,在 x86 上,普通的全尺寸加载和存储到自然对齐的变量确实是原子的。但如果没有std::atomic,编译器没有义务发出普通的全尺寸加载和存储;它有权聪明地以其他方式访问这些变量。它“知道”这不会有问题,因为并发访问将是数据竞争,当然程序员永远不会编写带有数据竞争的代码,不是吗?

作为一个具体示例,请考虑以下代码:

unsigned x;

unsigned foo(void) {
    return (x >> 8) & 0xffff;
}
Run Code Online (Sandbox Code Playgroud)

加载一个漂亮的 32 位整数变量,然后进行一些算术运算。还有什么比这更无辜的呢?但请查看 GCC 11.2-O2 尝试 godbolt发出的程序集:

foo:
        movzx   eax, WORD PTR x[rip+1]
        ret
Run Code Online (Sandbox Code Playgroud)

哦亲爱的。部分加载,且未对齐启动。

幸运的是,x86 确实保证在 P5 Pentium 或更高版本上,即使未对齐,对齐双字中包含的 16 位加载或存储也是原子的。事实上,任何适合对齐 8 字节的 1、2 或 4 字节加载或存储在 x86-64 上都是原子的,因此即使是x这样,这也将是有效的优化std::atomic<int>。但在这种情况下,GCC 就会错过优化。

Intel和AMD分别对此做出保证。Intel 适用于 P5 Pentium 及更高版本,其中包括所有 x86-64 CPU。没有一个“x86”文档列出了原子性保证的公共子集。堆栈溢出答案列表结合了这两个供应商的保证;想必它对于 Via / 兆信等其他供应商来说也是原子的。

希望在任何将 x86 指令转换为 AArch64 机器代码的模拟器或二进制翻译器中也能得到保证,但如果主机上没有匹配的原子性保证,这绝对是值得担心的事情。


这是另一个有趣的示例,这次是在 ARM64 上。根据 ARMv8-A 架构参考手册的 B2.2.1,对齐的 64 位存储是原子的。所以这看起来不错:

unsigned long x;

void bar(void) {
    x = 0xdeadbeefdeadbeef;
}
Run Code Online (Sandbox Code Playgroud)

但是,GCC 11.2 -O2 给出了(godbolt):

bar:
        adrp    x1, .LANCHOR0
        add     x2, x1, :lo12:.LANCHOR0
        mov     w0, 48879
        movk    w0, 0xdead, lsl 16
        str     w0, [x1, #:lo12:.LANCHOR0]
        str     w0, [x2, 4]
        ret
Run Code Online (Sandbox Code Playgroud)

这是两个 32 位str,无论如何都不是原子的。读者可以很好地阅读0x00000000deadbeef

为什么要这样做呢?在具有固定指令大小的 ARM64 上,在寄存器中实现 64 位常量需要多条指令。但该值的两半是相等的,那么为什么不具体化 32 位值并将其存储到每一半呢?

(如果你unsigned long *p; *p = 0xdeadbeefdeadbeef这样做,那么你会得到stp w1, w1, [x0]godbolt)。这看起来更有希望,因为它是一条指令,但在基础 ARMv8-A 中,实际上它仍然是两个单独的写入,以实现线程之间的原子性。LSE2 功能,可选ARMv8.2-A 和 ARMv8.4-A 中的强制要求,在合理的对齐条件下确实具有ldp/stp原子性。)


用户 supercat 对“带有隔离的并发无序写入是否对共享内存未定义行为?”的回答 有另一个关于 ARM32 Thumb 的好例子,其中 C 源代码要求unsigned short加载一次,但生成的代码加载它两次。在存在并发写入的情况下,您可能会得到“不可能”的结果。

人们可以在 x86-64 ( godbolt )上引发同样的情况:

_Bool x, y, z;

void foo(void) {
    _Bool tmp = x;
    y = tmp;
    // imagine elaborate computation here that needs lots of registers
    z = tmp;
}
Run Code Online (Sandbox Code Playgroud)

GCC 将重新加载x而不是溢出tmp。在 x86 上,您只需使用一条指令即可加载全局,但溢出到堆栈则需要至少两条指令。因此,如果x通过线程或信号/中断同时修改,那么assert(y == z)之后可能会失败。


假设任何超出语言实际保证的事情确实是不安全的,除非使用std::atomic. 现代编译器非常了解语言规则的确切限制,并积极优化。他们可以而且将会破坏假设他们会做“自然”的事情的代码,如果这超出了语言承诺的范围,而且他们经常会以人们从未想到的方式去做。

  • @GabrielStaples:除非我在编译器手册中看到明确的承诺,否则我不会感到安全。C/C++ 的本质是“我从来没有遇到过问题”并不是“它是正确的”的有力证据。在我的 ARM64 示例中,您可以一生都将常量存储到 64 位变量并发现它们是原子的,直到有一天有人将“0xdeadbeefdeadbeee”更改为“0xdeadbeefdeadbeef”,然后您就可以享受莫名其妙的错误报告。 (3认同)
  • 是的,在单处理器系统中的单核 CPU 上,通常只需要中断方面的原子性,这大致相当于我的第一个要点中的“信号方面的原子性”。值得注意的是,数据竞争规则不适用,并且定义“易失性 sig_atomic_t”就足够了。仅仅“易失性”本身*通常*就足以满足您的需要;编译器更有可能正式或非正式地承诺这一点。 (2认同)
  • *据我所知,x86 不提供有关未对齐加载的原子性承诺。* - 实际上,AMD 和 Intel 的保证的公共子集确实保证了在 P5 Pentium 或更高版本上加载对齐双字的中间 16 位的原子性,甚至从不可缓存的内存也是如此。[为什么在 x86 上自然对齐的变量上的整数赋值是原子的?](/sf/answers/2567953951/) 在*一般*中,未对齐的字加载不是原子的,因为它们可能会跨越更宽的边界。但在 32 位块内始终是安全的,就像在可缓存内存中的 qword 内一样。 (2认同)

Lun*_*din 9

在 8 位 AVR 微控制器(例如:Arduino Uno 或 Mini 使用的 ATmega328 mcu)上,只有 8 位数据类型具有原子读取和写入。

仅当您使用汇编语言而不是 C 语言编写代码时。

在(32 位)STM32 微控制器上,任何 32 位或更小的数据类型绝对自动是原子的。

仅当您使用汇编程序而不是 C 语言编写代码时。此外,仅当 ISA 保证生成的指令是原子的时,我不记得这是否适用于所有 ARM 指令。

其中包括 bool/_Bool、int8_t/uint8_t、int16_t/uint16_t、int32_t/uint32_t、float 和所有指针。

不,这绝对是错误的。

现在我需要了解我的 64 位 Linux 计算机。哪些类型绝对自动是原子的?

与 AVR 和 STM32 中相同的类型:无。

这一切都归结为 C 中的变量访问不能保证是原子的,因为它可能在多条指令中执行。或者在某些情况下,ISA 不保证原子性的指令。

C(和 C++)中唯一可以被视为原子的类型是带有_AtomicC11/C++11 限定符的类型。时期。

我在 EE 的这个答案重复的。它明确解决了微控制器情况、竞争条件、使用volatile、危险优化等。它还包含一种简单的方法来防止中断中的竞争条件,适用于中断不能被中断的所有 MCU。引用该答案:

编写 C 语言时,必须保护 ISR 和后台程序之间的所有通信免受竞争条件的影响。总是,每一次,无一例外。MCU 数据总线的大小并不重要,因为即使您用 C 语言进行单个 8 位复制,该语言也无法保证操作的原子性。除非您使用 C11 功能,否则不会_Atomic。如果此功能不可用,则必须使用某种方式的信号量或在读取期间禁用中断等。内联汇编器是另一种选择。易失性不保证原子性。


归档时间:

查看次数:

1787 次

最近记录:

2 年,4 月 前