23

There is a somewhat famous Unix brain-teaser: Write an if expression to make the following program print Hello, world! on the screen. The expr in if must be a legal C expression and should not contain other program structures.

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

The answer is fork().

When I was younger, I just had a laugh and forgot about it. But rethinking it, I find I couldn't understand why this program is surprisingly reliable than it should be. The order of execution after fork() is not guaranteed and a race condition exists, but in practice, you almost always see Hello, world!\n, never world!\nHello,.

To demonstrate it, I ran the program for 100,000 rounds.

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

On Linux 5.9 (Fedora 32, gcc 10.2.1, -O2), after 100001 executions, the child only won 146 times, the parent has a winning probability of 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

The result is similar on FreeBSD 12.2 (clang 10.0.1, -O2). The child only won 68 times, or 0.00067% of the time, meanwhile the parent won 99.993% of all executions.

An interesting side-note is that ktrace ./fork instantly changes the dominant result to world\nHello, (because only the parent is traced), demonstrating the Heisenbug nature of the problem. Nevertheless, tracing both processes via ktrace -i ./fork reverts the behavior back, because both processes are traced and equally slow.

$ 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

Independence from Buffering?

An answer suggests that buffering can influence the behavior of this race condition. But the behavior still presents after removing \n from printf().

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

And turning off stdout's buffering via stdbuf on 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

Why does printf() in the parent almost always win the race condition after fork() in practice? Is it related to the internal implementation details of printf() in the C standard library? The write() system call? Or process scheduling in the Unix kernels?

比尔盖子
  • 2,693
  • 5
  • 37
  • 53
  • Does this answer your question? [printf anomaly after "fork()"](https://stackoverflow.com/questions/2530663/printf-anomaly-after-fork) – Umair Mubeen Jan 14 '21 at 07:05
  • 3
    @UmairMubeen This question is less about the buffering issues and more about the heuristics the OS uses to decide which process should run. – Some programmer dude Jan 14 '21 at 07:23
  • @UmairMubeen No, I don't think buffering is responsible. – 比尔盖子 Jan 14 '21 at 07:23
  • 10
    `fork` is probably not a scheduling preemption point. So unless the parent happens to exhaust its time slice right after `fork` it's likely to finish executing everything before the child even runs. – kaylum Jan 14 '21 at 07:23
  • 1
    @kaylum Makes perfectly sense. I found running a few CPU-time wasting `xz /dev/urandom -c > /dev/null` (forcing the scheduling to run more often) is enough to dramatically increase the likelihood of the child winning in the race condition. Now the child wins 16116 times in 100001 executions, boosting the winning probability from 0.001% to 10%. – 比尔盖子 Jan 14 '21 at 08:23
  • 4
    N.B., this works as well: `if (!printf("Hello, "))` (I know, this was not your actual question ...) – chtz Jan 14 '21 at 09:19

2 Answers2

18

When fork is executed, the process executing it (the new parent) is executing (of course), and the newly created child is not. For the child to run, either the parent must be stopped and the child given the processor, or the child must be started on another processor, which takes time. Meanwhile, the parent continues execution.

Unless some unrelated event occurs, such as the parent exhausting the time slice it was given for sharing the processor, it wins the race.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • 2
    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). – David Schwartz Jan 14 '21 at 11:16
  • 1
    @DavidSchwartz I just tried the program in the original 4.3BSD running on a DEC VAX-780 simulator and got the same result, the parent always wins, I must be overthinking about it. How old are "the old days" that you were referring to? – 比尔盖子 Jan 14 '21 at 19:22
  • It's not true that the child process must wait for the parent to be blocked or descheduled for the child to start running (but it's still true that the parent is ready to run before the child, for the reasons I explain in my answer). in a true multiprocessor or multicore system, both processes can be each being executed at the same time in diferent processors/cores. – Luis Colorado Jan 16 '21 at 10:35
  • 1
    @LuisColorado: This answer does not state the child process must wait for the parent to be blocked or descheduled. It explicitly states a possibility is the child runs on another processor. – Eric Postpischil Jan 16 '21 at 11:04
  • 1
    Even if the child process runs in another processor (this is the most probable thing in a multiprocessor system) the parent is unblocked as soon as the system has the new process id for the child, while the child mus wait for his process to be fully built, making it to have to wait a bit more to be freed to run. – Luis Colorado Jan 19 '21 at 08:27
  • @DavidSchwartz I now understood what you meant, you were referring to `vfork()` (and it was indeed originally came from BSD). My 4.3BSD's man page reads "vfork differs from fork in that the child borrows the parent's memory and thread of control until a call to execve(2) or an exit (either by a call to exit(2) or abnormally.) The parent process is suspended while the child is using its resources." – 比尔盖子 Jan 20 '21 at 10:29
  • It is my recollection that even before `vfork` existed, the general rule was to allow the child to run first in the hope that it would call an `exec` function fairly quickly and thus avoid having to unshare a whole lot of dirtied pages by allowing the parent to keep running. – David Schwartz Jan 20 '21 at 18:22
  • @DavidSchwartz Interesting. Could you name a system that did that? I'd love to see this curious historical behavior in a simulator. – 比尔盖子 Jan 22 '21 at 17:43
-1

When you execute printf(3) to output a string to the terminal (to any tty device, this is checked inside the stdio package, by means of a isatty(3) call), the stdio package works in line mode buffering, which means that the internal buffer that accumulates output before writing it to the terminal flushes the buffer:

  • if the buffer fills up completely (this is not going to happen, as the string is too short, while the buffer is typically a best performance size or around 16kb ---this is the value for ufs2 filesystems in BSD unix), or...
  • if the output contains a \n line separator (this is only happening in the parent code, see below) the flush occurs at the position of the \n.

As your parent code (the one that received the pid_t process id of the child) is the one that executes the printf(3) with the included \n character, it's buffer is flushed at the time of the execution of the printf() call, while the buffer of the child will be flushed at the time of the exit(3) system call, as part of the atexit(3) processing. You can test this by calling _exit(2) (the exit(3) version that doesn't call the at-exit handlers) in both, the parent and the child, and you will see that only the parent output is seen on the screen.

There is, as you tell, a race condition, so in case the child is executed to its end, before the parent has had time to execute its printf(3) then you can get the parent's output at the end (just put a sleep(3) call in the parent code, before the printf(3), and you will see the correct order. The most important thing is that the first process that starts it's write(2) system call will be the winner (because the inode is locked during the execution of the write(2) syscal, and the output is sequenced). But the parent process only executes it's code without any system call in between, while the sequence for the child process is to store the string in the buffer and to flush it when the list of atexit(3) functions is called after returning from main(). This can involve several system calls in the mean time, that can even block the process for a while.

You can also put a \n in the child code and it is probable that you can see the child process being scheduled and starting the write() before the parent, although it is still probable that the parent will continue winning because it is very possible that it gets scheduled before the child is allowed to start (this is because the parent starting the fork(2) executes only the first part of it, e.g. checking for permissions to create a child and create the new process table entry that gives it the child's pid number needed to return from the fork, allowing the parent's fork(2) to return as soon as the child process id is known, while the allocation of memory segments to the new process and prepare it to execute is done in the child's fork() second half. This means that most probably the child will return from the fork() call when the parent is already running at top speed to the printf() call. But you cannot control this.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31
  • Re “As your parent code (the one that received the `pid_t` process id of the child) is the one that executes the `printf(3)` with the included `\n` character”: The `printf` with the new-line character is in the `else` clause, which is executed when the `fork` returns zero, which is in the child. – Eric Postpischil Jan 16 '21 at 11:07
  • @EricPostpischil, anyway, I have explained all the involved reasons that can make the child process to lose the race. All of them contribute, despite I have just misinterpreted the first. Being the one that has the `\n` is not determinant, and also can be an issue. You are right in that the `\n` is in the else part, my apologies for that. Anyway, as the PO indicates, there's a race condition that can make the child process to win the race. But my last paragraph about the fork() call can make the child to be unblocked far far time after the parent. – Luis Colorado Jan 19 '21 at 08:25