A call that returns twice, and a corpse that needs collecting
◈ 12 cardsCreate a child with fork, collect its status with wait, decode the status word with the macros, and both create and prevent a zombie.
fork() returns twice, and that is not a metaphor
#include <unistd.h>
pid_t fork(void);
One call, three possible return values:
- 0 — you are in the child.
- a positive number — you are in the parent, and this is the child's PID.
- −1 — the fork failed; there is no child, and
errnosays why (EAGAINfor a process limit,ENOMEMfor no swap).
The "returns twice" line sounds like a riddle until you put the events in order. fork() duplicates the calling process — address space, descriptor table, environment, working directory, umask, signal dispositions, everything — and the duplicate exists before fork() has finished executing. So the call has to finish in both processes. It does, and it hands each of them a different answer. Both then continue at the very same statement: the assignment that captures the return value.
That is why the standard shape is a three-way branch on one variable:
pid_t pid = fork();
if (pid == -1) perror("fork");
else if (pid == 0) /* child */ ;
else /* parent */ ;
The child inherits a copy of the descriptor table, so files opened before the fork are shared through the same open-file-table entries — one offset between the two of them. It gets its own PID, a new parent, reset resource counters, and exactly one thread of execution even if the parent had many.
Collecting a child: wait and waitpid
#include <sys/wait.h>
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
Both return the PID of the child they collected, or −1. wait() blocks until some child terminates. waitpid() names which: −1 for any child (which makes it wait()), a positive PID for that child specifically, 0 for any child in the caller's process group. Its options argument takes WNOHANG, which turns the call non-blocking — it returns 0 immediately if there is nothing to report, instead of waiting.
The status word, and the macros that decode it
The int filled in through stat_loc is not the exit status. It is a packed word carrying two different pieces of news:
- The high byte is the value the child passed to
exit(). - The low byte is the reason the child terminated: 0 for a normal exit, or the number of the signal that killed it, with an extra bit set if a core was dumped.
So a child that ran exit(3) leaves 0x0300, which as an integer is 768. A child killed by Ctrl-C leaves SIGINT in the low byte. Comparing the raw int against 3 is therefore wrong twice over: wrong value, and no way to tell an exit from a killing.
Decode it with the macros from <sys/wait.h> and never by hand:
int status;
pid_t child = wait(&status);
if (WIFEXITED(status))
printf("%d exited with %d\n", child, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("%d was killed by signal %d\n", child, WTERMSIG(status));
WIFEXITED is true for a normal exit and WEXITSTATUS then gives the exit() argument. WIFSIGNALED is true for a death by signal and WTERMSIG then gives the signal. They are mutually exclusive, and one of them is always true for a terminated child. Use the names, not the bit arithmetic — the layout is not identical on every system, and the macros are the portable interface to it.
Zombies
When a process exits, the kernel releases nearly everything it owned: its memory, its descriptors, its open files. One thing is kept: the proc structure, because that is where the exit status now lives, and the parent has not read it yet.
A process in that condition — terminated, status not yet collected — is a zombie. ps shows it in state Z with the command as <defunct>. It uses no CPU and no memory; what it occupies is a slot in the process table, and a program that forks in a loop without ever waiting will eventually exhaust that table.
The zombie disappears the moment its status is read. Two things can do that:
- The parent calls
wait()orwaitpid(). It reads the status, the kernel frees theprocstructure, and the entry is gone. - The parent dies. The orphaned zombie is adopted by
init(PID 1), which does nothing all day butwait(). So no child ofinitever stays a zombie — which is why killing the parent is the crude way to clear a screenful of them.
Making one on purpose therefore needs both halves: a child that exits immediately, and a parent that stays alive and does not wait. Drop either and there is no zombie — if the parent waits, the status is collected; if the parent exits, init collects it.
Preventing them in a server
A program that must keep working cannot sit in wait(). The standard fix is to reap from a SIGCHLD handler, draining every finished child without blocking on any of them:
void reap(int sig) {
int saved = errno;
while (waitpid(-1, NULL, WNOHANG) > 0)
;
errno = saved;
}
Install it with sigaction(), and never with signal():
struct sigaction sa;
sa.sa_handler = reap;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
Under the System V semantics signal() inherited, the disposition is reset to the default the moment the handler is entered, so the second child to die is never reaped and the leak the handler exists to prevent comes straight back. sigaction() keeps the handler installed, sigemptyset(&sa.sa_mask) says no extra signal is blocked while it runs, and SA_RESTART resumes a slow system call the delivery interrupted instead of failing it with EINTR. Module 13 takes this interface apart in full.
The loop matters: several children can die while the handler is not running, and signals do not queue — one SIGCHLD may stand for three deaths. WNOHANG makes the loop terminate when there is nothing left instead of blocking on the next child. Saving and restoring errno matters too, because the handler can interrupt a system call in the main flow and clobber the value it was about to read.
Many textbook servers omit all of this and quietly accumulate zombies for as long as they run.