Skip to content

Process Termination, Zombies, and wait()

Process termination doesn’t remove a process from the system the moment it stops running: fork and exec cover half of a process’s life cycle, creation, and this article covers the other half, what actually happens once a process exits and why the kernel can’t simply discard it right away.

A process terminates by calling exit (directly, or implicitly by returning from main), passing an integer exit status the kernel has to preserve, since a parent process may want to know later whether its child succeeded or failed and how. That status has nowhere to live once the process’s own memory is gone, so the kernel keeps it in the process table entry itself, the same structure Multitasking already covers as holding a task’s scheduling state, and everything the terminated process actually owned, its address space, open file descriptors, allocated memory, is released immediately regardless of whether anyone has asked for the exit status yet.

void do_exit(int status) {
free_address_space(current->mm);
close_all_files(current->files);
current->exit_status = status;
current->state = ZOMBIE;
notify_parent(current->parent, SIGCHLD);
schedule(); // never returns
}

A process that has released everything it owned but whose exit status hasn’t been collected yet is a zombie: its process table entry survives specifically to hold that status, ps still lists it, but it consumes no CPU time, no memory beyond the table entry itself, and runs no code at all. This is deliberate, not a bug or a resource leak: the process table entry is the only place left for the exit status to live, and a kernel that freed it immediately on exit would have nowhere to return that status from once the parent actually asks for it via wait.

fork() ──► running ──► exit() ──► ZOMBIE ──► parent calls wait() ──► entry freed
└─ SIGCHLD delivered to parent here

A zombie that’s never collected stays a zombie indefinitely, which is a real, if slow-building, resource leak: each one holds a process table slot and a PID that can’t be reused, and a long-running parent that forks children without ever calling wait on them will eventually exhaust the system’s process table entirely, an easy mistake to make in a shell or supervisor process that spawns children faster than it reaps them.

A parent retrieves a terminated child’s exit status by calling wait or waitpid, which blocks (unless a non-blocking flag is given) until at least one of its children has become a zombie, then returns that child’s PID and exit status while simultaneously freeing the process table entry, ending the zombie’s existence.

pid_t child = fork();
if (child == 0) {
exit(42); // child becomes a zombie carrying status 42
} else {
int status;
waitpid(child, &status, 0); // blocks until the child exits, then reaps it
int code = WEXITSTATUS(status);
}

IPC already covers SIGCHLD as the asynchronous notification a parent receives the moment a child becomes a zombie, which is what lets a parent call wait in response to an event rather than polling for child termination on some fixed schedule; a parent that ignores SIGCHLD entirely and never calls wait at all is exactly the failure mode that accumulates zombies indefinitely.

A child whose parent exits (or is killed) before the child does becomes an orphan, and the kernel handles this by immediately reparenting it to a designated ancestor, conventionally the init process (PID 1), rather than leaving it with no parent to eventually collect its exit status at all. This guarantee is what keeps an orphan from becoming a permanent, unreapable zombie the moment it eventually exits: init is written specifically to call wait in a loop, collecting the exit status of every orphan reparented to it (and every zombie it produces itself) as a matter of course, whether or not it has any other relationship to that process.

Delivering SIGCHLD and updating the parent pointer both have to happen atomically with respect to the parent’s own termination: a child exiting at the exact moment its parent is also exiting has to be reparented before the kernel finishes tearing down the original parent, not after, or the child would briefly point at a parent structure that no longer exists. WIFEXITED, WIFSIGNALED, and the other macros around waitpid’s status value exist because a single integer encodes two genuinely different pieces of information packed together, whether the process exited normally versus was killed by a signal, and which specific value (exit code or signal number) applies in each case, a distinction a caller has to check before interpreting the raw status bits directly.

  1. ^ W. R. Stevens and S. A. Rago, Advanced Programming in the UNIX Environment, Chapter 8: process termination, the zombie state, and the wait/waitpid family in full.
  • fork and exec: the creation half of the process life cycle this article’s termination half completes.
  • IPC: the SIGCHLD signal a parent uses to learn a child has become a zombie without polling for it.