The idea: every process has a parent who must collect its exit status
On Linux every process except PID 1 is created by another process, its parent. When a process ends, it can't vanish at once: its parent may still want to know how it ended (exit code 0? killed by a signal?). So the kernel frees the process's memory and files but keeps a tiny entry with its PID and its exit status. That leftover is a zombie. It goes away only when the parent calls wait() and reads the status. This is called reaping.
The canvas shows a small process tree (who is whose parent), the process table the way ps -o pid,ppid,stat,cmd prints it, what the kernel is doing in the task_struct, and a log of the system calls in strace -f style.
fork() and execve(): a new PID vs a new program
fork() makes a copy of the calling process. The copy gets the next free PID, and its PPID (parent PID) is the caller. fork() returns the child's PID in the parent and 0 in the child. How the memory is shared rather than copied is on Copy-on-Write fork().
execve() does something else: it replaces the program inside the same process. The PID, the parent and the open files (unless marked close-on-exec) stay; the code, data and stack are new. That is why a shell runs a command as fork() + execve(): the child becomes ls, and bash stays bash. How the new program is loaded is on How Linux Loads a Program. Demo: fork → exec → exit → wait.
How a shell wires two such children together with pipe(), dup2() and close() before execve() is on How the Shell Runs ls | grep foo.
exit(): what is freed and what stays
exit(code) (the system call exit_group) runs do_exit() in the kernel. It frees the address space, closes every file, and releases locks and timers. What stays is the task_struct with exit_state = EXIT_ZOMBIE and exit_code, the status word the parent will read. Then the kernel sends the parent SIGCHLD.
A zombie uses no CPU and no memory beyond that small entry. What it does use is a PID. PIDs are limited (/proc/sys/kernel/pid_max, 4194304 on 64-bit) and so are processes per user (ulimit -u) and per cgroup (pids.max). Enough zombies and fork() fails with EAGAIN.
wait, waitpid, WNOHANG and the status word
wait(&status) blocks until some child has ended, then returns its PID and fills in status. The entry becomes EXIT_DEAD (X) and is removed: the PID is free. waitpid(pid, &status, options) waits for one child (or any, with -1). With WNOHANG it never sleeps: it returns 0 when no child has finished yet. Programs with an event loop call waitpid(-1, &s, WNOHANG) in a loop from their SIGCHLD handler.
The status is one integer. Read it with the macros, never by hand:
| Macro | Meaning | Example |
|---|---|---|
WIFEXITED(s) | the child called exit() or returned from main | exit(3) → s = 0x0300 → true |
WEXITSTATUS(s) | the exit code, 0–255 | 3 |
WIFSIGNALED(s) | a signal killed it | kill -9 → s = 0x0009 → true |
WTERMSIG(s) | which signal | 9 (SIGKILL); the shell shows $? = 128 + 9 = 137 |
Zombies: <defunct> in ps
A parent that never calls wait() leaves its dead children as zombies: ps shows STAT Z and the name as [worker] <defunct>. kill -9 does nothing to them: they are already dead, there is no code left to stop. There are two ways to clear them: make the parent call wait() (fix its code, or send it SIGCHLD if it has a handler), or end the parent, so the zombies are passed to PID 1, which reaps them. Find the parent with ps -o ppid= -p <pid>. Demo: parent never waits → zombie.
Orphans, reparenting and subreapers
If a parent ends before its children, the children are orphans. The kernel never leaves a process without a parent: in exit_notify() it gives each orphan a new one. It first looks up the tree for the nearest ancestor that called prctl(PR_SET_CHILD_SUBREAPER, 1); if there is none, the new parent is PID 1 of the PID namespace. systemd as PID 1 reaps every child it gets. Demos: parent dies first; a subreaper adopts the orphan.
Subreapers exist so that a supervisor can keep track of everything it started, even grandchildren whose parents died: systemd --user, tini -s and containerd-shim set it.
SIGCHLD set to SIG_IGN
A parent that never wants to know how its children ended can say so: signal(SIGCHLD, SIG_IGN) (or the SA_NOCLDWAIT flag). Then the kernel doesn't keep zombies at all: children go straight to EXIT_DEAD and are released at exit, no SIGCHLD is sent, and wait() fails with ECHILD once no child is left. The default action for SIGCHLD is also "ignore", but that default does not auto-reap; only setting it explicitly does. How signals are delivered and handled is on Linux Signals. Demo: SIGCHLD = SIG_IGN → no zombies.
PID 1 in a container
A container gets its own PID namespace, and the first process in it is PID 1 of that namespace. With docker run image node app.js that is your app. Orphans inside the container are given to it, but most apps only wait for the children they started themselves (Node's libuv calls waitpid() for each PID it spawned). Every adopted orphan that ends stays a zombie, and they pile up until pids.max is hit and fork() fails. A typical cause is a shell script that starts something in the background and exits. More on namespaces and pids.max on What a Container Really Is. Demo: container, PID 1 never reaps.
The fix is a tiny init as PID 1: docker run --init starts tini (docker-init) as PID 1, which starts your app as its child, reaps every zombie and forwards signals like SIGTERM to the app. In Kubernetes, shareProcessNamespace: true makes the pause container PID 1, which also reaps. Demo: container with --init (tini).
On a normal host, PID 1 is systemd (see How Linux Boots for how it gets there), and it reaps everything.
The double-fork daemon
Old-style daemons detach from the terminal with two forks: the process forks, the child calls setsid() and forks again, and the middle process exits at once. The grandchild is an orphan, so it ends up under PID 1, and the original parent only has to reap the short-lived middle child. Under systemd, services don't need this any more: systemd starts them directly as its own children.
See also Process vs Thread: what a child process copies and a thread shares, and why a crash kills one process but every thread.
The R, S and D states in ps decide which processes count in the load average: see Linux Load Average, Explained.
Sessions, process groups and the controlling terminal that setsid() leaves behind are animated on TTY, PTY and Job Control: What Ctrl-C Really Does. Under systemd a service doesn't need to double-fork at all: systemd starts it, tracks it in a cgroup and restarts it if it crashes; see systemd Units: Dependencies, Restart and Socket Activation.
What the page leaves out
Threads: a process can have many threads (a thread group with one tgid), and exit_group ends them all; the page shows one thread per process. It also leaves out clone() flags, vfork() and posix_spawn(), waitid() and pidfds (pidfd_open, P_PIDFD), process groups and sessions (job control, SIGHUP), stopped and traced states (T, t), uninterruptible sleep (D), and PID reuse after the counter wraps at pid_max. Memory after fork() is on Copy-on-Write fork(), signal delivery on Linux Signals, and program loading on How Linux Loads a Program.