繁体   English   中英

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

[英]Which types on a 64-bit computer are naturally atomic in gnu C and gnu C++? -- meaning they have atomic reads, and atomic writes

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

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

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 版本在 C11 中具有_Atomic类型,在 C++11 中具有std::atomic<>类型)。 这包括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;

因此, int显然是原子的。

并且 Gnu++ 标头<bits/types.h>包含在<ext/atomicity.h>中,并存储在我的计算机上的/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;

所以,再一次, 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
    }
}

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

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

const _Atomic int32_t i;
// or (same thing)
const atomic_int_least32_t i;

看这里:

  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;

看这里:

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

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

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

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


2022 年 4 月 14 日更新

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

  1. 你使用汇编。
  2. 您使用 C11 _Atomic类型或 C++11 std::atomic<>类型。
  3. 您将 gcc 编译器与 gnu 语言和 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. <util/atomic>部分中的 AVR-libc 用户手册支持我的说法,即AVR 上的 8 位类型在由 gcc 编译时,已经具有自然原子读取自然原子写入,当它暗示 8 位读取和写入时通过说已经是原子的(强调添加):

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

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

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

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

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

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

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


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

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

unsigned x;

unsigned foo(void) {
    return (x >> 8) & 0xffff;
}

加载一个不错的 32 位整数变量,然后是一些算术。 还有什么比这更无辜? 但是请查看 GCC 11.2 -O2发出的程序集,尝试使用 godbolt

foo:
        movzx   eax, WORD PTR x[rip+1]
        ret

哦亲爱的。 部分负载,并且未对齐引导。 AFAIK x86 没有提供关于未对齐负载的原子性承诺。


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

unsigned long x;

void bar(void) {
    x = 0xdeadbeefdeadbeef;
}

但是,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

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

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

(如果你执行unsigned long *p; *p = 0xdeadbeefdeadbeef那么你会得到stp w1, w1, [x0] ( godbolt )。这看起来更有希望,因为它是一条指令,但实际上仍然是两个单独的写入线程之间的原子性。)


用户 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;
}

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


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

在 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++)中,唯一可以被视为原子的类型是具有 C11/C++11 中的_Atomic限定符的类型。 时期。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM