Difficulty: Intermediate
How do fork() and exec() work? How many times is 'hi' printed in a program that calls fork() three times, and what does the output look like when stdout is buffered?
Fork output questions are a staple of campus placement tests and technical rounds, and once you know the mental model they become mechanical. Let's build the model first. fork() creates a new process by duplicating the calling process. After fork returns, there are two nearly identical processes, parent and child, each continuing from the instruction right after the fork call. The only visible difference is the return value: fork returns the child's PID (a positive number) in the parent, 0 in the child, and -1 on failure. That single distinction lets one piece of code branch into two roles.
Modern kernels do not physically copy all memory on fork. They use copy-on-write: parent and child initially share the same physical pages, marked read-only, and the kernel copies a page only when either process writes to it. That makes fork cheap. It also explains why changing a variable in the child never changes the parent's copy: they have separate virtual address spaces even though the physical pages start out shared.
exec() is a family of calls (execl, execv, execvp and so on) that replaces the current process image with a new program. The PID stays the same, but the code, data, heap and stack are replaced, and execution starts at the new program's main. If exec succeeds, it never returns; any line after it runs only if exec failed. The classic shell pattern is fork then exec in the child, and wait in the parent: that is precisely how your shell launches ls.
Now the counting rules for output questions. If a process calls fork() n times in a row without conditions, the total number of processes is 2 to the power n, so the number of new processes is 2^n - 1. Three consecutive forks give 8 processes, so a printf after them runs 8 times. For fork inside an if with the return value, trace who takes which branch. For loops such as for(i=0;i<3;i++) fork(); the count is also 2^3 = 8 processes because children continue the loop with the same counter value.
The second trap is output buffering. When stdout is a terminal it is line-buffered, so text ending with a newline is flushed immediately. When stdout is redirected to a file or a pipe, it is fully buffered, and unflushed text sitting in the buffer gets copied into the child at fork time. So printf("Hello") without a newline, followed by fork(), prints Hello twice at exit when piped, because both processes flush their own copy of the buffer. Always mention that behaviour is implementation-dependent and can be avoided with fflush(stdout) before fork.
Finally, ordering: the order in which parent and child print is not deterministic unless you synchronize with wait(). When an interviewer asks for the output, list the possibilities or state that the order can vary, and then give the count of lines, which is deterministic.
#include <stdio.h>
#include <unistd.h>
int main(void) {
fork();
fork();
fork();
printf("hi\n");
return 0;
}
Each fork doubles the number of processes: 1 -> 2 -> 4 -> 8. Every process executes the printf.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int x = 10;
pid_t pid = fork();
if (pid == 0) {
x += 5;
printf("child x=%d\n", x);
} else {
wait(NULL); /* parent waits for child */
printf("parent x=%d\n", x);
}
return 0;
}
Copy-on-write gives the child its own copy of x when it modifies it. wait() forces the child line to appear first.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
execlp("ls", "ls", "-l", (char *)NULL);
perror("exec failed"); /* runs only if exec fails */
return 1;
}
wait(NULL);
printf("parent: child finished\n");
return 0;
}
After a successful execlp the child's image is replaced by ls, so the perror line never runs.
fork, exec, wait, process creation, output prediction