【发布时间】:2021-04-19 05:12:44
【问题描述】:
有一个有点著名的 Unix 脑筋急转弯:写一个if 表达式,让下面的程序在屏幕上打印Hello, world!。 if 中的 expr 必须是合法的 C 表达式,并且不应包含其他程序结构。
if (expr)
printf("Hello, ");
else
printf("world!\n");
答案是fork()。
我年轻的时候,只是笑了笑就忘记了。但是重新考虑它,我发现我无法理解为什么这个程序比它应该的更可靠。 fork() 之后的执行顺序无法保证,并且存在竞争条件,但在实践中,您几乎总是会看到 Hello, world!\n,而不是 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
独立于缓冲?
答案表明缓冲会影响这种竞争条件的行为。但是从 printf() 中删除 \n 后,该行为仍然存在。
if (expr)
printf("Hello");
else
printf("World");
并在 FreeBSD 上通过 stdbuf 关闭标准输出的缓冲。
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() 几乎总是在fork() 之后赢得比赛条件?是不是和C标准库中printf()的内部实现细节有关? write() 系统调用?还是 Unix 内核中的进程调度?
【问题讨论】:
-
这能回答你的问题吗? printf anomaly after "fork()"
-
@UmairMubeen 这个问题不是关于缓冲问题,而是关于操作系统用来决定应该运行哪个进程的启发式方法。
-
fork可能不是调度抢占点。因此,除非父进程恰好在fork之后用完其时间片,否则它很可能在子进程运行之前完成所有操作。 -
@kaylum 完全有道理。我发现运行一些 CPU 时间浪费
xz /dev/urandom -c > /dev/null(迫使调度更频繁地运行)足以显着增加孩子在比赛条件下获胜的可能性。现在孩子在 100001 次处决中获胜 16116 次,获胜概率从 0.001% 提升到 10%。 -
注,这也适用:
if (!printf("Hello, "))(我知道,这不是你的实际问题......)
标签: c unix fork race-condition