比尔盖*_*尔盖子 23 c unix fork race-condition
有一个有点著名的 Unix 脑筋急转弯:写一个if表达式,使下面的程序打印Hello, world!在屏幕上。该expr中if必须是合法的C表达式,不应包含其他的程序结构。
if (expr)
printf("Hello, ");
else
printf("world!\n");
Run Code Online (Sandbox Code Playgroud)
答案是fork()。
当我年轻的时候,我只是笑了笑,忘记了。但是重新思考它,我发现我无法理解为什么这个程序比它应该的可靠得惊人。之后的执行顺序fork()无法保证并且存在竞争条件,但在实践中,您几乎总是看到Hello, world!\n,从不world!\nHello,。
为了证明这一点,我运行了 100,000 轮程序。
for i in {0..100000}; do
./fork >> log
done
Run Code Online (Sandbox Code Playgroud)
在 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
Run Code Online (Sandbox Code Playgroud)
结果在 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
Run Code Online (Sandbox Code Playgroud)
一个答案表明缓冲会影响这种竞争条件的行为。但是\n从 printf() 中删除后,该行为仍然存在。
if (expr)
printf("Hello");
else
printf("World");
Run Code Online (Sandbox Code Playgroud)
并通过stdbufFreeBSD关闭标准输出的缓冲。
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
Run Code Online (Sandbox Code Playgroud)
为什么printf()在父母fork()练习后几乎总是赢得比赛条件?是否与printf()C 标准库中的内部实现细节有关?该write()系统调用?或者 Unix 内核中的进程调度?
Eri*_*hil 17
当fork被执行时,执行它的进程(新的父进程)正在执行(当然),而新创建的子进程则不是。要让子进程运行,要么必须停止父进程并为子进程分配处理器,要么必须在另一个处理器上启动子进程,这需要时间。同时,父级继续执行。
除非发生一些不相关的事件,例如父级耗尽了它为共享处理器而提供的时间片,否则它会赢得比赛。