11

I was studying re-entrancy in programming. On this site of IBM (really good one). I have founded a code, copied below. It's the first code that comes rolling down the website.

The code tries showing the issues involving shared access to variable in a non linear development of a text program (asynchronicity) by printing two values that constantly change in a "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;
   }
}

The problems appeared when I tried to run the code (or better, didn't appear). I was using gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1) in default configuration. The misguided output doesn't occurs. The frequency in getting "wrong" pair values is 0!

What is going on after all? Why there is no problem in re-entrancy using static global variables?

4

2 回答 2

18

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

        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 来说可能是正确的,但正如代码所示,现在并非如此。将结构更改为具有两个长而不是两个整数会显示问题。

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

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

于 2020-01-16T17:57:17.357 回答
12

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

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

请参阅MCU 编程 - C++ O2 优化在循环时中断 - 单核微控制器上的中断处理程序与单线程程序中的信号处理程序基本相同。在这种情况下,UB 的结果是负载被吊出循环。

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

在您的情况下,编译器可以从无限循环中优化存储,因为没有 UB-free 程序可以观察到它们。 data不是_Atomicorvolatile,并且循环中没有其他副作用。因此,任何读者都无法与该作者同步。实际上,如果您在启用优化的情况下进行编译(Godbolt在 main 的底部显示一个空循环),就会发生这种情况。我还将 struct 更改为 two long long,并且 gcc 在循环之前使用单个movdqa16 字节存储。(这不能保证是原子的,但实际上在几乎所有 CPU 上都是如此,假设它是对齐的,或者在 Intel 上只是不跨越缓存线边界。 为什么在 x86 上对自然对齐的变量进行整数赋值是原子的?

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

volatile struct two_int还会强制编译器不要优化它们,但不会强制它以原子方式加载/存储整个结构。(不过,它也不会阻止它这样做。)请注意,这volatile不能避免数据竞争 UB,但实际上它足以用于线程间通信,并且是人们构建手动原子(以及内联 asm)的方式在 C11 / C++11 之前,适用于普通 CPU 架构。它们是缓存连贯的,因此volatile在实践中与纯加载和纯存储最相似_Atomicmemory_order_relaxed,如果用于足够窄的类型,编译器将使用一条指令,这样你就不会被撕裂。而且当然volatile_Atomic与编写使用和 mo_relaxed编译为相同 asm 的代码相比,ISO C 标准没有任何保证。


如果你有一个函数global_var++;在一个intlong long你从主运行从信号处理程序异步运行的函数上执行,那将是一种使用重入来创建数据竞争 UB 的方法。

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

指令中间不会发生中断或其他异常,因此内存目标添加是原子的。在单核 CPU 上进行上下文切换。在单核 CPU 上,只有一个(缓存一致的)DMA 写入器可以从add [mem], 1没有前缀的 a中“踩上”一个增量。lock没有其他线程可以在其上运行的任何其他内核。

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

于 2020-01-17T03:32:22.380 回答