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

3df52oht  于 2022-11-12  发布在  其他
关注(0)|答案(2)|浏览(145)
  • 注意:对于这个问题,我不是在谈论C或C++语言标准,而是在谈论针对特定架构的gcc编译器实现,因为语言标准对原子性的唯一保证是在C11或更高版本中使用_Atomic类型,或者在C++11或更高版本中使用std::atomic<>类型。另请参阅我在这个问题底部的更新。*

在任何架构上,某些数据类型都可以自动读取和写入,而其他数据类型则需要多个时钟周期,并且可能在操作过程中中断,如果数据在线程之间共享,则会导致数据损坏。
8位单核AVR微控制器上(例如:Arduino Uno、Nano或Mini所使用的ATmega 328 MCU),只有8位数据类型具有原子读写(使用gcc编译器和 gnu C或 gnu C语言)。我在〈2天内完成了25小时的调试马拉松,然后是wrote this answer here。另请参见此问题的底部以了解更多信息。当使用使用AVR-libc库的gcc编译器进行编译时,具有用于AVR 8位微控制器的自然原子写入和自然原子读取的位变量。
日期**(32位)STM32单核微控制器**,任何数据类型32位或更小绝对自动实现原子(当使用gcc编译器和gnuC或gnuC 语言进行编译时,as *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. Which variable types/sizes are atomic on STM32 microcontrollers?
  2. Reading a 64 bit variable that is updated by an ISR
  3. What are the various ways to disable and re-enable interrupts in STM32 microcontrollers in order to implement atomic access guards?
    现在我需要知道我的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“原子”类型提供了原子读和原子写 * 以及 * 原子递增运算符、递减运算符和compound assignment...
    ......但我说的不是这个。
    我想知道哪些类型具有自然原子读和自然原子写。对于我所谈论的,递增、递减和复合赋值 * 不是 * 自然原子的。

2022年4月14日更新

我与ST的某个人进行了一些交谈,STM32微控制器似乎只保证在以下条件下对特定大小的变量进行原子读写:
1.您使用组件。
1.可以使用C11 _Atomic类型或C++11 std::atomic<>类型。
1.您将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/--〉“用户手册”链接。
  2. AVR-libc user manual in the <util/atomic> section * 支持我的主张 *,即 * AVR上的8位类型在由gcc编译时 * 已经 * 具有 * 自然原子读取 * 和 * 自然原子写入 *,当它暗示8位读取和写入已经是原子时,可以这样说(着重号是添加的):
    需要原子访问的一个典型示例是16(或更多)位变量,它在主执行路径和ISR之间共享。
    1.它谈论的是C代码,而不是汇编,因为它在该页上给出的所有示例都是用C语言编写的,包括紧跟在引号后面的volatile uint16_t ctr变量的示例。
wrrgggsh

wrrgggsh1#

答案从语言标准的Angular 来看很简单:它们中没有一个是“绝对自动”原子
首先,区分“原子”的两种含义是很重要的。

  • 一个是 * 关于信号的原子 *。这确保了,例如,当你在sig_atomic_t上执行x = 5时,在当前线程中调用的信号处理程序 * 将看到旧值或新值。这通常通过在一条指令中执行访问来简单地完成,因为信号只能由硬件中断触发,而硬件中断只能在指令之间到达。例如,x86 add dword ptr [var], 12345,即使没有lock前缀,在这个意义上也是原子的。
  • 另一个是 * 原子的线程 *,这样另一个同时访问对象的线程就会看到一个正确的值。这是比较难得到正确的。特别是,sig_atomic_t类型的普通变量不是原子的线程。你需要_Atomicstd::atomic来得到它。

请注意,您的实现为其类型选择的内部名称并不是任何东西的证据。我不知道实现者在什么意义上使用“原子”这个词,或者它是否准确(例如,可以被遗留代码使用)。如果他们想做出这样的承诺,它应该在 * 文档 * 中,而不是在一个bits头文件中的一个未解释的typedef中,这个头文件永远不会被应用程序程序员看到。
你的硬件可能会使某些类型的访问“自动原子化”,这一事实并不能告诉你C/C++级别的任何事情。例如,在x86上,对自然对齐变量的普通全尺寸加载和存储是原子的。但是在没有std::atomic的情况下,编译器没有义务 * 发出 * 普通全尺寸加载和存储;它“知道”这不会有问题,因为并发访问将是一场数据竞赛,当然,程序员永远不会用数据竞赛来编写代码,对吗?
作为一个具体示例,请考虑以下代码:

unsigned x;

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

加载一个漂亮的32位整型变量,后面跟着一些算术运算。还有什么比这更简单的呢?请查看GCC 11.2 -O2try on godbolt发出的程序集:

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

天哪。部分负载,而且没有对齐。
幸运的是,x86确实保证了包含在对齐的双字中的16位加载或存储在P5 Pentium或更高版本上是原子的,即使是未对齐的。事实上,任何适合对齐的8字节的1、2或4字节加载或存储在x86-64上都是原子的。因此即使xstd::atomic<int>,这也是一个有效的优化。但是在这种情况下,GCC会错过优化。
Intel和AMD都分别对此做出了保证。Intel针对P5 Pentium和更高版本,包括所有x86-64 CPU。没有一个“x86”文档列出原子性保证的公共子集。堆栈溢出答案列表结合了这两个供应商的保证;可能在薇娅/兆新等其他厂商上也是原子级的。
希望在将x86指令转换为AArch 64机器代码的任何模拟器或二进制转换器中也能得到保证,例如,但如果主机上没有匹配的原子性保证,这肯定是要担心的。
这是另一个有趣的例子,这次是在ARM 64上。根据《ARMv 8-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
为什么要这样做呢?在ARM 64上,将一个64位常量具体化到一个寄存器中需要几条指令,因为它的指令大小是固定的。但是,值的两半是相等的,所以为什么不具体化32位值并将其存储到每半中呢?
(If如果执行unsigned long *p; *p = 0xdeadbeefdeadbeef,则会得到stp w1, w1, [x0]godbolt)。这看起来更有希望,因为它是一条指令,但实际上,出于线程之间原子性的目的,仍然是两个单独的写入。)
用户supercat对Are concurrent unordered writes with fencing to shared memory undefined behavior?的回答有另一个很好的ARM 32 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。现代编译器非常清楚语言规则的确切限制,并且积极地进行优化。他们可以并且将会破坏那些假设他们将做“自然”的事情的代码,如果这超出了语言所承诺的界限。而且他们经常以人们从未预料到的方式这样做。

mspsb9vt

mspsb9vt2#

在8位AVR微控制器上(例如:Arduino Uno或Mini使用的ATmega 328 MCU),只有8位数据类型具有原子读取和写入。
只有在你用汇编语言而不是C语言写代码的情况下。
在(32位)STM32微控制器上,任何32位或更小的数据类型都是绝对自动原子的。
只有在你用汇编语言而不是C语言编写代码的情况下。另外,只有在伊萨保证生成的指令是原子指令的情况下,我不记得这是否对所有ARM指令都是正确的。
这包括bool/_Bool、int8_t/uint8_t、int16_t/uint16_t、int32_t/uint32_t、浮点数和所有指针。
不,那绝对是错的。
现在我需要知道对于我的64位Linux计算机。哪些类型是明确自动原子的?
与AVR和STM32中的类型相同:没有。
这一切都归结为,C语言中的变量访问不能保证是原子的,因为它可能在多条指令中执行,或者在某些情况下,在ISA不保证原子性的指令中执行。
在C(和C++)中,* 唯一 * 可以被视为原子类型的是那些带有来自C11/C++11的_Atomic限定符的类型。
我在EE here上的这个答案是重复的。它明确地解决了微控制器的情况、争用条件、volatile的使用、危险的优化等。它还包含一种简单的方法来防止中断中的争用条件,适用于所有中断不能中断的MCU。引用该答案:
在编写C时,ISR和后台程序之间的所有通信都必须防止竞争条件。* 始终 *,每次都不例外。MCU数据总线的大小无关紧要,因为即使您在C中执行单个8位复制,该语言也无法保证操作的原子性。除非您使用C11特性_Atomic。如果该特性不可用,你必须使用某种方式的信号量或在读取等过程中禁用中断。内联汇编程序是另一种选择。volatile不保证原子性。

相关问题