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


11

我正在研究编程的重入性。在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;
   }
}

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

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


1
确保禁用所有编译器优化,然后重试
roaima

我以为...但是我会改变哪些选择?我不知道。:-(
Daniel Bandeira

5
这看起来像一个编程问题(堆栈溢出)。它似乎没有放在这里。(对不起,我的子站点较少;它是如此划分的。但是就是这样。)
ctrl-alt-delor

1
最简单的重入代码是不可变的。
ctrl-alt-delor

起初,我认为这个问题与gcc和Linux环境有关。例如,不断发展的OS调度(在调用处理程序例程之前,在中断信号之后执行更多程序文本)。
丹尼尔·班迪拉

Answers:


12

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

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

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

实际上,由于数据争用UB而导致的撕裂测试用例实际上是在32位模式下开发的,或者是通过较旧的dudu编译器(分别加载结构成员)开发/测试的。

在您的情况下,编译器可以从无限循环中优化存储,因为没有UB-free程序无法观察到它们。 data不是_Atomicvolatile,并且循环中没有其他副作用。因此,任何读者都无法与该作家进行同步。实际上,如果启用了优化功能进行编译,就会发生这种情况(Godbolt在main底部显示一个空循环)。我还将结构更改为2 long long,并且gcc movdqa在循环之前使用了一个16字节的存储区。(这不能保证是原子的,但是实际上几乎所有的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++;上执行的功能,int或者long long您是从main上运行的,并且是从信号处理程序异步运行的,那么这将是使用重入方式创建数据争用UB的一种方式。

取决于它是如何编译的(到内存目标inc或add,或分开的load / inc / store),对于同一线程中的信号处理程序,它是否是原子的。请参阅num ++是否可以为'int num'原子?有关x86和C ++中原子性的更多信息。(C11的stdatomic.h_Atomic属性提供与C ++ 11 std::atomic<T>模板等效的功能)

指令中间不会发生中断或其他异常,因此内存目标地址是原子wrt。上下文在单核CPU上切换。在单核CPU上,只有(高速缓存相关的)DMA写入器可以从“ add [mem], 1lock前缀”的“增量”开始。没有其他线程可以在其上运行的其他内核。

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


2
尽管伊卡鲁(Icaru)的回答对我来说足够了,但我还是被迫接受您的最佳答案。您告诉我们的清晰概念为我提供了一整天(和以后)学习的主题。实际上,乍一看我几乎不了解您在前两段中所写的内容。谢谢!如果您在互联网上公开有关计算机和编程的文章,请给我们链接!
丹尼尔·班迪拉

17

查看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.,对于典型的cpus在2005年可能是正确的,但是如代码所示,现在还不正确。将结构更改为具有两个long而不是两个int会显示此问题。

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

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


3
尝试将数据类型从更改intlong long,然后编译为32位。上一课是,您永远不知道它是否/何时会破裂。
ctrl-alt-delor

2
这意味着在我的机器中,这两个值的分配是原子操作?(考虑x86_64体系结构的编译)
Daniel Bandeira

1
long long 对于x86-64仍可编译为一条指令:16字节 movdqa。除非您禁用优化,例如在Godbolt链接中。(GCC的默认设置为-O0调试模式,该模式充满了存储/重新加载的噪声,通常看起来并不有趣。)
Peter Cordes

阅读所有评论后,我将类型更改为“ long long”。结果很有趣:获得了等待的结果,并且设置了一些计数器,由于不匹配的数据的速率如何受到其余代码的影响,它可以改善其他概念。谢谢大家的帮助!
丹尼尔·班迪拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.