为什么父进程中的 printf() 在 fork() 之后几乎总是赢得竞争条件?

比尔盖*_*尔盖子 23 c unix fork race-condition

有一个有点著名的 Unix 脑筋急转弯:写一个if表达式,使下面的程序打印Hello, world!在屏幕上。该exprif必须是合法的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被执行时,执行它的进程(新的父进程)正在执行(当然),而新创建的子进程则不是。要让子进程运行,要么必须停止父进程并为子进程分配处理器,要么必须在另一个处理器上启动子进程,这需要时间。同时,父级继续执行。

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

  • 在过去,情况恰恰相反。绝大多数机器只有一个核心,首先运行子进程可以让它在非常常见的情况下快速执行“exec”,从而避免复制父进程修改的每一页内存当它恢复时(如果孩子想访问以前的内容)。 (2认同)