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
- Processes, PCB and Process States: PCB, process states, zombie and orphan.
- C:
if, loops,&&and||with short-circuit evaluation,printf.
Core concepts
The four calls
| Call | Effect | Returns |
|---|---|---|
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 counter | In 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 kept | Nothing 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 state | The child's PID, or -1 if there are no children |
void exit(int status) | Terminates the caller after flushing stdio buffers and running atexit handlers | Does 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
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. consecutive fork() calls, executed unconditionally, give processes in total, that is 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: ifAis 0 (false),Bis not evaluated.A || B: ifAis non-zero (true),Bis 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 forks once. A child created in iteration continues with iteration , so it has children of its own.

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 be the number of processes after for (i = 0; i < n; i++) fork();. Every process forks once per iteration, so with , giving .
Children per process. In the same loop, a process created in iteration (0-based) forks in iterations to , so it has children; the original has . The tree is the binomial tree , whose levels hold processes at depth . For : depths 0 to 3 hold processes.
Lines printed inside a loop. If each iteration forks once and then prints once, iteration prints lines, and the total over iterations is .
Worked examples
Assume every fork() succeeds and stdout is a terminal (line-buffered) unless stated.
| Code | Processes in total | Lines printed |
|---|---|---|
for (i = 0; i < 3; i++) fork(); | 8 | 0 |
fork(); fork(); printf("hi\n"); | 4 | 4 |
if (fork() && fork()) fork(); | 4 | 0 |
fork() || fork(); | 3 | 0 |
fork() && (fork() || fork()); | 4 | 0 |
for (i = 0; i < 2; i++) { fork(); printf("x\n"); } | 4 | 6 |
for (i = 0; i < 4; i++) if (fork() == 0) break; | 5 | 0 |
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 iterations prints 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
| Code | Processes in total | Lines printed |
|---|---|---|
fork() && fork() || fork(); | 5 | 0 |
fork(); fork() && fork(); fork(); | 12 | 0 |
for (i = 0; i < 3; i++) if (fork() == 0) printf("x\n"); | 8 | 7 |
for (i = 1; i <= 4; i++) if (i % 2 == 0) fork(); | 4 | 0 |
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 . 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 calls are made: 7 lines and 8 processes.
A fork that runs only on some iterations. In the last loop fork() runs only for and : two doublings, processes. Count the iterations on which the fork executes, not the iterations of the loop.
Variables are not shared
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
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 is | Buffering | Output |
|---|---|---|
| A terminal | Line-buffered: A\n written before the fork | A once, B twice |
| A pipe or a file | Fully buffered: A\n still in the buffer at the fork | A 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
forkreturns twice (once in each process) andexecreturns 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
waitleaves zombies; the kernel keeps their process-table entries. vfork(historical) andposix_spawnare alternatives that avoid copying; with copy-on-write,forkis already cheap.
Common misconceptions.
- "The child starts executing from the beginning of
main." It continues from theforkcall. - "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: total, children. With conditions, track the return value and short-circuit evaluation; drawing the tree is the safest method.
- Counting output lines. Multiply each
printfby 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
execnever run; the PID does not change. - Zombies and orphans. A child that exits before its parent calls
waitis a zombie; a child whose parent exits first is adopted byinit.
Summary
Sources
- 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).
- 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.