IBM 示例代码,不可重入函数在我的系统中不起作用

IBM example code, non re-entrant functions doesn't work in my system

我正在研究编程的重新进入。 IBM的这个site上(真不错)。我创建了一个代码,复制如下。这是网站上滚动的第一个代码。

该代码试图通过打印两个在 "dangerous context".

中不断变化的值来显示涉及在文本程序的非线性开发(异步性)中共享访问变量的问题
#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;
   }
}

当我尝试 运行 代码时出现了问题(或者更好的是,没有出现)。我在默认配置中使用 gcc 版本 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)。错误的输出不会发生。获得"wrong"对值的频率为0!

到底是怎么回事?为什么使用静态全局变量重入没有问题?

查看 godbolt 编译器资源管理器(在添加缺失的 #include <unistd.h> 之后),可以看到对于几乎所有 x86_64 编译器,生成的代码都使用 QWORD 移动来加载 oneszeros 在一条指令中。

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

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 将显示问题。

我之前写过这是 "atomic" 懒惰的。该程序仅在单个 cpu 上 运行。从这个 cpu 的角度来看,每条指令都将完成(假设没有其他任何东西改变内存,例如 dma)。

因此在 C 级别未定义编译器将选择单个指令来编写结构,因此可能会发生 IBM 论文中提到的损坏。针对当前 cpus 的现代编译器确实使用单个指令。一条指令足以避免单线程程序的损坏。

那不是真的重新进入;您不会 运行 在同一个线程 (或不同线程)中两次调用一个函数。您可以通过递归或将当前函数的地址作为回调函数指针 arg 传递给另一个函数来获得它。 (而且它不会不安全,因为它是同步的)。

这只是信号处理程序和主线程之间的普通数据争用 UB(未定义行为):只有 sig_atomic_t 对于此 是安全的。其他人可能会碰巧工作,比如在你的情况下,可以在 x86-64 上用一条指令加载或存储一个 8 字节的对象,而编译器恰好选择了那个 asm。 (正如@icarus 的回答所示)。

参见 MCU programming - C++ O2 optimization breaks while loop - 单核微控制器上的中断处理程序与单线程程序中的信号处理程序基本上是一样的。在那种情况下,UB 的结果是负载从循环中吊起。

由于数据争用 UB 而实际发生撕裂的测试用例可能是在 32 位模式下开发/测试的,或者是使用单独加载结构成员的较旧的 dumber 编译器开发/测试的。

在您的情况下,编译器可以从无限循环中优化存储,因为无 UB 的程序永远无法观察到它们。 data 不是 _Atomicvolatile,循环中没有其他副作用。 所以任何 reader 都不可能与作者同步。如果您在启用优化的情况下进行编译 (Godbolt shows an empty loop at the bottom of main). I also changed the struct to two long long, and gcc uses a single movdqa 16-byte store before the loop. (This is not guaranteed atomic, but it is in practice on almost all CPUs, assuming it's aligned, or on Intel merely doesn't cross a cache-line boundary. Why is integer assignment on a naturally aligned variable atomic on x86?)

,就会发生这种情况

因此在启用优化的情况下进行编译也会破坏您的测试,并且每次都会向您显示相同的值。 C 不是可移植的汇编语言。

volatile struct two_int 也会强制编译器不要优化它们,但 不会 强制它以原子方式 load/store 整个结构。 (不过,它也不会 阻止 它这样做。)请注意 volatile 确实 而不是 避免数据争用 UB,但在实践中,它足以进行线程间通信,并且人们在 C11 / C++11 之前为正常的 CPU 架构构建了手动原子(以及内联 asm)。它们是缓存一致的,所以 volatile 对于纯加载和纯存储是 in practice mostly similar to _Atomic with memory_order_relaxed,如果用于足够窄的类型,编译器将使用一条指令,这样你就不会撕裂。当然,volatile 与编写使用 _Atomic 和 mo_relaxed.

编译为相同 asm 的代码相比,ISO C 标准没有任何保证

如果您有一个函数在 intlong long 上执行 global_var++; 而您 运行 来自 main 并且 与信号处理程序异步,这将是一种使用重入来创建数据争用 UB 的方法。

根据它的编译方式(到内存目标 inc 或 add,或分离 load/inc/store),对于同一线程中的信号处理程序,它是原子的还是非原子的。有关 x86 和 C++ 中原子性的更多信息,请参阅 。 (C11 的 stdatomic.h_Atomic 属性提供与 C++11 的 std::atomic<T> 模板等效的功能)

指令中间不能发生中断或其他异常,因此内存目标添加是原子的。单核 CPU 上的上下文切换。只有(高速缓存一致的)DMA 编写器可以 "step on" 从 add [mem], 1 的增量,在单核 CPU 上没有 lock 前缀。没有其他线程可以 运行 开启的任何其他内核。

所以它类似于信号的情况:信号处理程序运行s 而不是处理信号的线程的正常执行,所以它不能在一条指令的中间处理。