Introduction

In UNIX-like systems a new process is made in two separate steps. fork() creates a near-exact copy of the calling process; exec() then replaces the copy's program with a new one. A shell runs a command by forking, letting the child exec the command, and waiting for the child to finish.

Separating the two steps is what makes UNIX process control flexible: between fork and exec the child can redirect its input and output, close files or change its user, and the parent's state is untouched. It is also the basis of a counting exercise: given a few lines of C with fork() calls, how many processes exist and what is printed?

The exact semantics, a counting method that works for loops and short-circuit conditions, and the output-buffering trap are the points that decide the process count and the printed output.

Prerequisites

Core concepts

The four calls

CallEffectReturns
pid_t fork(void)Creates a child that is a copy of the caller: same code, copy of data, heap and stack, copies of open file descriptors, same program counterIn the parent: the child's PID (positive). In the child: 0. On failure: -1 in the parent, no child
exec family, for example execlp(file, arg0, ..., NULL)Replaces the calling process's program and memory image; PID and open descriptors (unless marked close-on-exec) are keptNothing on success (the old code is gone); -1 on failure
pid_t wait(int *status)Blocks until some child terminates; collects its exit status, ending its zombie stateThe child's PID, or -1 if there are no children
void exit(int status)Terminates the caller after flushing stdio buffers and running atexit handlersDoes not return

After fork() both processes continue from the same point: the return from fork. Both resume at the fork return point with different return values; the child also has a new process identity and the documented inheritance differences. Which one runs first is up to the scheduler and must not be assumed.

Separate copies. After the fork, parent and child have separate copies of every variable. A change made by one is invisible to the other. A pointer printed in both shows the same virtual address, which maps to different physical frames once either process writes. Open files are different: parent and child share the same open-file entry, and therefore the same file offset.

Copy-on-write. Modern kernels do not copy memory at fork. Eligible private writable pages are shared and temporarily write-protected for copy-on-write; a write creates a private copy as needed. Originally read-only pages remain protected, while explicitly shared mappings remain shared. A fork followed at once by exec therefore copies almost nothing. See Virtual Memory and Demand Paging.

A typical shell

C
pid_t pid = fork();
if (pid < 0) {               /* fork failed */
    perror("fork");
} else if (pid == 0) {       /* child */
    execlp("ls", "ls", "-l", (char *)NULL);
    perror("exec");          /* reached only if exec failed */
    _exit(1);
} else {                     /* parent */
    int status;
    waitpid(pid, &status, 0);
}

Code after a successful execlp never runs in the child. _exit is used in the failed child so that it does not flush a copy of the parent's stdio buffers.

How to count processes

Rule 1: straight-line forks double

Each fork() executed by every existing process doubles the count. nn consecutive fork() calls, executed unconditionally, give 2n2^{n} processes in total, that is 2n−12^{n} - 1 new ones.

Rule 2: follow the return value

Inside a condition, the parent sees a non-zero value (true) and the child sees 0 (false). Then apply C's short-circuit rules:

  • A && B: if A is 0 (false), B is not evaluated.
  • A || B: if A is non-zero (true), B is not evaluated.

So in fork() && fork(), the first child (which got 0) skips the second fork; in fork() || fork(), the first parent (non-zero) skips it.

Rule 3: draw the tree for loops

For for (i = 0; i < n; i++) fork(); every process alive at iteration ii forks once. A child created in iteration ii continues with iteration i+1i + 1, so it has n−1−in - 1 - i children of its own.

Process tree for three forks in a loop
Figure 1. The process tree of for (i = 0; i < 3; i++) fork();: P creates P0, P1 and P2; P0 creates P01 and P02; P1 creates P12; P01 creates P012; 8 processes in total, with P2, P02, P12 and P012 as leaves.

Mathematical explanation

Counting with recurrences. Let T(n)T(n) be the number of processes after for (i = 0; i < n; i++) fork();. Every process forks once per iteration, so T(n)=2 T(n−1)T(n) = 2\,T(n - 1) with T(0)=1T(0) = 1, giving T(n)=2nT(n) = 2^{n}.

Children per process. In the same loop, a process created in iteration ii (0-based) forks in iterations i+1i + 1 to n−1n - 1, so it has n−1−in - 1 - i children; the original has nn. The tree is the binomial tree BnB_n, whose levels hold (nk)\binom{n}{k} processes at depth kk. For n=3n = 3: depths 0 to 3 hold 1,3,3,11, 3, 3, 1 processes.

Lines printed inside a loop. If each iteration forks once and then prints once, iteration ii prints 2i+12^{i+1} lines, and the total over nn iterations is ∑i=0n−12i+1=2n+1−2\sum_{i=0}^{n-1} 2^{i+1} = 2^{n+1} - 2.

Worked examples

Assume every fork() succeeds and stdout is a terminal (line-buffered) unless stated.

CodeProcesses in totalLines printed
for (i = 0; i < 3; i++) fork();80
fork(); fork(); printf("hi\n");44
if (fork() && fork()) fork();40
fork() || fork();30
fork() && (fork() || fork());40
for (i = 0; i < 2; i++) { fork(); printf("x\n"); }46
for (i = 0; i < 4; i++) if (fork() == 0) break;50

Working for if (fork() && fork()) fork();. The original P forks C1. C1 gets 0, so the && is false and C1 is done. P gets non-zero and forks C2. C2 gets 0 and is done. P again gets non-zero, the condition is true, and P forks C3. Total: P, C1, C2, C3 = 4.

Working for fork() && (fork() || fork()). P forks C1; C1 gets 0 and stops. P evaluates fork() || fork(): it forks C2 and gets non-zero, so || is already true and P stops. C2 got 0, so it evaluates the second operand and forks C3. Total 4: P, C1, C2, C3.

Working for the printing loop. In iteration 0 there are 2 processes after the fork, each printing once: 2 lines. In iteration 1 both fork, giving 4 processes, each printing once: 4 lines. Total 6 lines. A loop of nn iterations prints 2+4+⋯+2n=2n+1−22 + 4 + \dots + 2^{n} = 2^{n+1} - 2 lines.

The fan loop. if (fork() == 0) break; makes each child leave the loop at once, so only the original process keeps forking: 4 children, 5 processes, and the tree is a star rather than a binomial tree.

Mixed operators and forks inside conditions

CodeProcesses in totalLines printed
fork() && fork() || fork();50
fork(); fork() && fork(); fork();120
for (i = 0; i < 3; i++) if (fork() == 0) printf("x\n");87
for (i = 1; i <= 4; i++) if (i % 2 == 0) fork();40

Working for fork() && fork() || fork(). It parses as (fork() && fork()) || fork(). P forks C1. In C1 the first fork() returned 0, so the && is false without evaluating its right side, and C1 goes on to the || and forks C2; C2 gets 0, which ends its expression. In P the first fork() was non-zero, so P forks C3. P gets non-zero, the && is true, and the || short-circuits: P stops. C3 got 0, so its && is false and it forks C4. Total: P, C1, C2, C3, C4 = 5.

Working for fork(); fork() && fork(); fork();. After the first line there are 2 processes. Each runs fork() && fork(), which turns 1 process into 3 (as in the if (fork() && fork()) row above, without the third fork), giving 2×3=62 \times 3 = 6. The last fork() doubles them: 12.

Working for the printing loop. Each fork() call creates exactly one child, and only the child prints, so the lines printed equal the number of fork() calls. Processes double each iteration (both parent and child continue the loop), so 1+2+4=71 + 2 + 4 = 7 calls are made: 7 lines and 8 processes.

A fork that runs only on some iterations. In the last loop fork() runs only for i=2i = 2 and i=4i = 4: two doublings, 22=42^2 = 4 processes. Count the iterations on which the fork executes, not the iterations of the loop.

Variables are not shared

C
int x = 5;
if (fork() == 0) { x += 10; printf("child %d\n", x); }
else { wait(NULL); x -= 1; printf("parent %d\n", x); }

Output: child 15 then parent 4. The child changes its own copy; the parent's x is still 5 when it subtracts 1. The wait forces the order; without it either line could come first.

The output-buffering trap

C
printf("A");
fork();
printf("B\n");

"A" has no newline, so it is still sitting in the parent's stdio buffer (in user memory) when fork() copies the address space. Both processes later flush their own copy, and the output is AB printed twice, not A once and B twice.

With printf("A\n") instead, the behaviour depends on where stdout goes:

stdout isBufferingOutput
A terminalLine-buffered: A\n written before the forkA once, B twice
A pipe or a fileFully buffered: A\n still in the buffer at the forkA and B from each process, so A twice and B twice

IMPERATIVE RULE. Count printf output by asking whether the text was already flushed before the fork. Text still in the buffer is duplicated by every fork that follows.

Important properties and observations

  • fork returns twice (once in each process) and exec returns only on failure.
  • The child inherits copies of open descriptors, the current directory, the environment and signal dispositions; it does not inherit the parent's pending alarms or file locks (POSIX).
  • A parent that never calls wait leaves zombies; the kernel keeps their process-table entries.
  • vfork (historical) and posix_spawn are alternatives that avoid copying; with copy-on-write, fork is already cheap.

Common misconceptions.

  • "The child starts executing from the beginning of main." It continues from the fork call.
  • "Parent and child share global variables." They share nothing writable except files and explicitly shared memory.
  • "The parent always runs first." The order is unspecified.

GATE examination perspective

  • Counting processes. Straight-line forks: 2n2^{n} total, 2n−12^{n} - 1 children. With conditions, track the return value and short-circuit evaluation; drawing the tree is the safest method.
  • Counting output lines. Multiply each printf by the number of processes alive when it executes, and check the buffering rule for text without a newline.
  • "Number of child processes created". The question may ask for new processes (exclude the original) or total processes; read which.
  • Values of variables. Each process has its own copy after the fork; the same virtual address does not mean the same physical memory.
  • exec semantics. Statements after a successful exec never run; the PID does not change.
  • Zombies and orphans. A child that exits before its parent calls wait is a zombie; a child whose parent exits first is adopted by init.

Summary

Sources

  1. A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10th ed., Wiley: Chapter 3, Section 3.3 Operations on Processes (process creation with fork, exec and wait; termination, zombies and orphans).
  2. A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4th ed., Pearson: Section 2.1.2 Process Creation, Section 2.1.4 Process Hierarchies, Section 1.6.1 System Calls for Process Management.

Next steps