首页 > 解决方案 > 为什么父级中的 printf() 几乎总是在 fork() 之后赢得竞争条件?

问题描述

有一个有点著名的 Unix 脑筋急转弯:写一个if表达式,让下面的程序打印Hello, world!在屏幕上。exprin必须是合法的ifC 表达式并且不应包含其他程序结构。

if (expr)
    printf("Hello, ");
else
    printf("world!\n");

答案是fork()

当我年轻的时候,我只是笑了笑就忘记了。但是重新考虑它,我发现我无法理解为什么这个程序比它应该的更可靠。之后的执行顺序fork()无法保证,并且存在竞争条件,但在实践中,您几乎总是会看到Hello, world!\n, never world!\nHello,

为了证明这一点,我运行了该程序 100,000 轮。

for i in {0..100000}; do
    ./fork >> log
done

在 Linux 5.9 (Fedora 32, gcc 10.2.1, -O2) 上,执行 100001 次后,孩子只赢了 146 次,父母赢的概率为 99.9985%。

$ uname -a
Linux openwork 5.9.14-1.qubes.x86_64 #1 SMP Tue Dec 15 17:29:47 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ wc -l log
100001 log

$ grep ^world log | wc -l
146

结果在 FreeBSD 12.2 (clang 10.0.1, -O2) 上是相似的。孩子只赢了 68 次,或 0.00067% 的时间,而父母赢了 99.993% 的所有处决。

一个有趣的旁注是ktrace ./fork立即将主要结果更改为world\nHello, (因为仅跟踪父项),证明了问题的 Heisenbug 性质。然而,通过跟踪这两个进程ktrace -i ./fork会将行为恢复回来,因为这两个进程都被跟踪并且同样慢。

$ uname -a
FreeBSD freebsd 12.2-RELEASE-p1 FreeBSD 12.2-RELEASE-p1 GENERIC  amd64

$ wc -l log 
100001 log

$ grep ^world log | wc -l
68

独立于缓冲?

一个答案表明缓冲可以影响这种竞争条件的行为。\n但是从 printf() 中删除后,该行为仍然存在。

if (expr)
    printf("Hello");
else
    printf("World");

stdbuf并通过FreeBSD关闭标准输出的缓冲。

for i in {0..10000}; do
    stdbuf -i0 -o0 -e0 ./fork >> log
    echo > log
done

$ wc -l log 
10001 log

$ grep -v "^HelloWorld" log | wc -l
30

为什么在亲本练习printf()后几乎总是赢得比赛条件?是不是和C标准库中fork()的内部实现细节有关?printf()系统write()调用?还是 Unix 内核中的进程调度?

标签: cunixforkrace-condition

解决方案


fork被执行时,执行它的进程(新的父进程)正在执行(当然),而新创建的子进程不是。要让子进程运行,要么必须停止父进程并为子进程分配处理器,要么必须在另一个处理器上启动子进程,这需要时间。同时,父进程继续执行。

除非发生一些不相关的事件,例如父级用尽了分配给它的共享处理器的时间片,否则它会赢得比赛。


推荐阅读