Gab*_*les 10 c c++ gcc x86-64 atomic
注意:对于这个问题,我不是在谈论 C 或 C++语言标准。相反,我谈论的是针对特定体系结构的 gcc 编译器实现,因为语言标准对原子性的唯一保证是使用_Atomic
C11 或更高版本中的类型或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
/ _Bool
、int8_t
/ uint8_t
、int16_t
/ uint16_t
、int32_t
/ uint32_t
、float
和所有指针。唯一的非原子类型是int64_t
/ uint64_t
、double
(8 字节)和long double
(也是 8 字节)。我在这里写过:
现在我需要知道我的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)
_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)
看这里:
C++11 及更高版本提供了std::atomic<>
如下类型:
const std::atomic<int32_t> i;
// or (same thing)
const atomic_int32_t i;
Run Code Online (Sandbox Code Playgroud)
看这里:
这些 C11 和 C++11“原子”类型提供原子读取和原子写入以及原子递增运算符、递减运算符和复合赋值...
......但这不是我真正要说的。
我想知道哪些类型具有自然原子读取和自然原子写入。对于我所说的,递增、递减和复合赋值不会自然地成为原子的。
我与 ST 的某人进行了一些聊天,似乎 STM32 微控制器只能保证在这些条件下对某些大小的变量进行原子读写:
_Atomic
类型或 C++11std::atomic<>
类型。到目前为止我的研究:
AVR gcc:不存在 avr gcc 编译器手册。相反,请使用此处的 AVR-libc 手册:https://www.nongnu.org/avr-libc/ -->“用户手册”链接。
<util/atomic>
支持了我的主张,即AVR 上的 8 位类型,当由 gcc 编译时,已经具有自然原子读取和自然原子写入,这意味着 8 位读取和写入已经是原子的: (强调):需要原子访问的典型示例是在主执行路径和 ISR 之间共享的16(或更多)位变量。
volatile uint16_t ctr
紧随该引用之后的变量示例。Nat*_*dge 20
从语言标准的角度来看,答案非常简单:它们都不是“绝对自动”原子的。
首先,区分“原子”的两种含义很重要。
一是对于信号来说是原子的。例如,这可以确保当您x = 5
对 a执行操作时,当前线程中volatile sig_atomic_t
调用的信号处理程序将看到旧值或新值。这通常只需在一条指令中进行访问即可完成,因为信号只能由硬件中断触发,而硬件中断只能在指令之间到达。例如, x86 ,即使没有前缀,在这个意义上也是原子的。add dword ptr [var], 12345
lock
另一个是关于线程的原子性,因此同时访问该对象的另一个线程将看到正确的值。这更难做到正确。特别是,普通类型的变量对于线程来说不是原子volatile sig_atomic_t
的。你需要_Atomic
或std::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
. 现代编译器非常了解语言规则的确切限制,并积极优化。他们可以而且将会破坏假设他们会做“自然”的事情的代码,如果这超出了语言承诺的范围,而且他们经常会以人们从未想到的方式去做。
在 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++)中唯一可以被视为原子的类型是带有_Atomic
C11/C++11 限定符的类型。时期。
我在 EE 的这个答案是重复的。它明确解决了微控制器情况、竞争条件、使用volatile
、危险优化等。它还包含一种简单的方法来防止中断中的竞争条件,适用于中断不能被中断的所有 MCU。引用该答案:
编写 C 语言时,必须保护 ISR 和后台程序之间的所有通信免受竞争条件的影响。总是,每一次,无一例外。MCU 数据总线的大小并不重要,因为即使您用 C 语言进行单个 8 位复制,该语言也无法保证操作的原子性。除非您使用 C11 功能,否则不会
_Atomic
。如果此功能不可用,则必须使用某种方式的信号量或在读取期间禁用中断等。内联汇编器是另一种选择。易失性不保证原子性。
归档时间: |
|
查看次数: |
1787 次 |
最近记录: |