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

有一个有点著名的 Unix 脑筋急转弯:写一个if表达式,使下面的程序打印Hello, world!在屏幕上。该exprif必须是合法的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立即将主要结果更改为worldnHello, (因为仅跟踪父项),证明了问题的 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");

并通过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

为什么printf()在父母fork()练习后几乎总是赢得比赛条件?是否与printf()C 标准库中的内部实现细节有关?该write()系统调用?或者 Unix 内核中的进程调度?

回答

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

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

  • In the old days, it was the other way around. The vast majority of machines only had one core, and running the child first allowed it to get to `exec` quickly in the very common case where that's what it was going to do, avoiding the need to copy every page of memory the parent modified when it resumed (in case the child wanted to access the previous contents).

以上是为什么父进程中的printf()在fork()之后几乎总是赢得竞争条件?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>