Dan*_*ira 11 c gcc signals x86-64 data-race
我正在学习编程的重入性。在IBM 的这个站点上(非常好)。我已经建立了一个代码,复制在下面。这是第一个在网站上滚动的代码。
该代码尝试通过打印在“危险上下文”中不断变化的两个值来显示涉及在文本程序的非线性开发(异步性)中共享访问变量的问题。
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
Run Code Online (Sandbox Code Playgroud)
当我尝试运行代码时出现问题(或者更好,没有出现)。我在默认配置中使用 gcc 版本 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)。不会发生被误导的输出。获得“错误”对值的频率为 0!
到底发生了什么?为什么使用静态全局变量重入没有问题?
ica*_*rus 18
查看Godbolt编译器资源管理器(在添加了缺失的 之后#include <unistd.h>),您会发现对于几乎所有 x86_64 编译器,生成的代码都使用 QWORD 移动在单个指令中加载ones和zeros。
mov rax, QWORD PTR main::ones[rip]
mov QWORD PTR data[rip], rax
Run Code Online (Sandbox Code Playgroud)
IBM 网站表示On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.,对于 2005 年的典型 CPU 而言,这可能是正确的,但正如代码所示,现在并非如此。将结构更改为具有两个 long 而不是两个 int 将显示问题。
我之前写过这是懒惰的“原子”。该程序仅在单个 CPU 上运行。从这个 cpu 的角度来看,每条指令都会完成(假设没有其他改变内存的东西,比如 dma)。
所以在C级别上没有定义编译器将选择单个指令来编写结构,因此可能会发生 IBM 论文中提到的损坏。针对当前 CPU 的现代编译器确实使用单个指令。单条指令足以避免单线程程序的损坏。
Pet*_*des 12
这并不是真正的可重入性;您不会在同一线程(或不同线程)中两次运行函数。您可以通过递归或将当前函数的地址作为回调函数指针 arg 传递给另一个函数来获得它。(而且它不会不安全,因为它是同步的)。
这只是信号处理程序和主线程之间的普通数据竞争 UB(未定义行为):仅sig_atomic_t保证 this 是安全的。其他人可能碰巧工作,例如在您的情况下,可以在 x86-64 上使用一条指令加载或存储 8 字节对象,并且编译器碰巧选择了该 asm。(正如@icarus 的回答所示)。
请参阅MCU 编程 - C++ O2 优化在循环时中断 - 单核微控制器上的中断处理程序与单线程程序中的信号处理程序基本相同。在那种情况下,UB 的结果是负载从环路中被提升。
由于数据竞争 UB 实际发生的撕裂测试用例可能是在 32 位模式下开发/测试的,或者使用单独加载结构成员的较旧的笨拙编译器。
在您的情况下,编译器可以从无限循环中优化存储,因为没有 UB-free 程序可以观察它们。 datais not _Atomicorvolatile,并且循环中没有其他副作用。因此,任何读者都无法与此作者同步。实际上,如果您在启用优化的情况下进行编译(Godbolt在 main 的底部显示一个空循环),就会发生这种情况。我还将 struct 更改为 two long long,并且 gccmovdqa在循环之前使用单个16 字节存储。(这不能保证原子性,但实际上在几乎所有 CPU 上都是如此,假设它是对齐的,或者在 Intel 上只是不跨越缓存线边界。 为什么在 x86 上自然对齐的变量原子上的整数赋值是原子的?)
因此,在启用优化的情况下进行编译也会破坏您的测试,并每次都显示相同的值。C 不是可移植的汇编语言。
volatile struct two_int也会强制编译器不优化它们,但不会强制它以原子方式加载/存储整个结构。(不过,它也不会阻止它这样做。)请注意,这volatile并不能避免数据竞争 UB,但实际上它足以用于线程间通信,并且是人们构建手动原子(以及内联 asm)的方式在 C11 / C++11 之前,用于普通 CPU 架构。他们在高速缓存相干所以volatile是在实践中大多是类似_Atomic与memory_order_relaxed纯负载和纯店,如果用于类型足够窄,编译器将使用一个单一的指令,这样你就不会得到撕裂。而且当然volatile与使用_Atomic和 mo_relaxed编译为相同 asm 的代码相比,ISO C 标准没有任何保证。
如果你有一个函数,做了global_var++;上int或long long您从主运行和从信号处理异步,这将是一个方式来使用重入创建数据争UB。
根据它的编译方式(到内存目标 inc 或 add,或分离加载/inc/store),对于同一线程中的信号处理程序,它是否是原子的。请参阅“int num”的 num++ 可以是原子的吗?有关 x86 和 C++ 中原子性的更多信息。(C11stdatomic.h和_Atomic属性提供与 C++11std::atomic<T>模板等效的功能)
指令中间不会发生中断或其他异常,因此内存目标添加是原子操作。在单核 CPU 上进行上下文切换。只有(缓存一致的)DMA 写入器才能在单核 CPU 上“踩踏”一个add [mem], 1没有lock前缀的增量。没有任何其他内核可以运行另一个线程。
所以它类似于信号的情况:信号处理程序运行而不是处理信号的线程的正常执行,因此它不能在一条指令的中间处理。