Reaping Orphans and the subreaper

When a process dies while its children are still alive, those children become orphans — they keep running, but the parent that was supposed to call wait() and collect their eventual exit status is gone. The kernel cannot simply leak them: every process must have someone responsible for reaping it, or its eventual zombie would pin a slot in the process table forever. So at exit time the kernel reparents each orphan to a designated reaper — by default PID 1, the system’s init, which is the universal ancestor of all userspace and whose central job is to reap whatever lands on it. The escape hatch is PR_SET_CHILD_SUBREAPER (prctl(2)): a process can mark itself a subreaper, and then orphaned descendants reparent to it instead of skipping all the way up to PID 1, letting a service manager or container init track the deaths of its own subtree (kernel/exit.c v6.12; PR_SET_CHILD_SUBREAPER). This note explains who reaps orphans and why PID 1 is special for it — the canonical failure being the container PID-1 zombie problem, where an application running as PID 1 forgets to reap and zombies pile up until the system runs out of PIDs.

This note is the boot/init framing of reaping: it focuses on PID 1’s reaping responsibility and the subreaper escape hatch as a property of init systems and containers. The byte-level kernel mechanism — the task_struct tree fields, find_new_reaper’s ancestor walk, process-group/session rules — is owned by The Process Tree and Reparenting, and the zombie lifecycle (EXIT_ZOMBIEwaitrelease_task) by Process Exit wait and Zombie Reaping. Here we connect those mechanics up to why init must do this (The Role and Responsibilities of init, PID 1 and the init Process).

Mental Model

Think of the process tree as a forest of responsibility: each parent has signed an implicit contract to wait() for its children. When a parent breaks that contract by dying first, the kernel does not tear down the orphaned subtree — that would be far too aggressive, since the orphans are still doing useful work. Instead it transfers the contract to a new responsible party. The default party is PID 1, which is the root of the whole tree and is built to honour the contract reliably (it loops on wait() forever). A subreaper is a process that volunteers to be a closer responsible party for its own descendants — so that, say, systemd learns of the death of a service’s double-forked daemon instead of that death being reported to system init, where systemd would never see it.

flowchart TB
  INIT["PID 1 — init / systemd<br/>default reaper, loops on wait()"]
  SUB["subreaper S<br/>prctl(PR_SET_CHILD_SUBREAPER,1)"]
  P["parent P (dies)"]
  O1["orphan O1 (still running)"]
  O2["orphan O2 (still running)"]
  INIT --> SUB
  SUB --> P
  P --> O1
  P --> O2
  P -. "P exits" .-> X(("reparent<br/>orphans"))
  X -. "nearest subreaper<br/>ancestor, NOT PID 1" .-> SUB
  SUB -. "later: O1/O2 exit →<br/>SIGCHLD to S; S must wait()" .-> SUB

Orphan reparenting and the subreaper escape hatch. What it shows: when parent P dies, the kernel walks up the ancestry of its orphaned children O1/O2 and reparents them to the nearest ancestor that set the child-subreaper flag (S) — only if no subreaper exists do they fall all the way to PID 1. The insight to take: reparenting does not finish the job — O1/O2 are still running; when they eventually exit they become zombies under their new parent, which must itself call wait(). The subreaper lets an interposed manager (a systemd instance, a container init) be that waiting party for its whole subtree, including descendants that deliberately double-forked to detach.

Why orphans need a reaper at all

A terminated process does not vanish: the kernel keeps a thin task_struct husk in the EXIT_ZOMBIE state holding the exit code and resource usage, and that husk only disappears when some process calls a wait-family system call to collect it — at which point release_task() frees the descriptor and recycles the numeric PID (the full lifecycle is Process Exit wait and Zombie Reaping). The implication is sharp: a zombie needs a living parent to reap it. If a process exits while its children are still alive and nobody adopts those children, then when they eventually die their zombies would have no parent to collect them — they would leak indefinitely. wait(2) (and the wait page) spells out the consequence: an unreaped zombie “will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes” — fork() starts failing with -EAGAIN and the system becomes unusable.

Reparenting closes this gap. The moment a parent dies, the kernel re-points every one of its still-living children at a new parent (the reaper), so the responsibility to wait() is never dropped on the floor. This is not an optional optimization — it is a load-bearing invariant of the process model, which is precisely why it is wired into the exit path itself.

The default reaper: PID 1

The default reaper is PID 1 of the dying task’s PID namespace — init. PID 1 is special to the kernel in several reinforcing ways, all of which exist so that it can be a trustworthy universal reaper:

  • It is the root of the tree. Every userspace process descends from PID 1 (rest_init and the Birth of PID 1 spawns it). When reparenting walks up the ancestor chain and finds no subreaper, the walk terminates at PID 1, which is therefore the backstop reaper for everything.
  • It cannot be killed by default-action signals. At fork time, when the kernel detects a process is becoming the namespace’s reaper, it sets the SIGNAL_UNKILLABLE flag: in copy_process(), if (is_child_reaper(pid)) { ns_of_pid(pid)->child_reaper = p; p->signal->flags |= SIGNAL_UNKILLABLE; } (kernel/fork.c v6.12). The signal-delivery path then honours this: in get_signal(), the kernel explicitly skips any non-kernel-only signal for an unkillable task — if (unlikely(signal->flags & SIGNAL_UNKILLABLE) && !sig_kernel_only(signr)) continue;, under the comment “Global init gets no signals it doesn’t want. Container-init gets no signals it doesn’t want from same container.” (kernel/signal.c v6.12). The practical upshot: SIGTERM and SIGINT sent to PID 1 are silently dropped unless PID 1 has explicitly installed a handler for them. Only SIGKILL/SIGSTOP are even checked — and even those, is_global_init() is forbidden to receive (a guard if (unlikely(is_global_init(t) && sig_kernel_only(sig))) return true; in sig_task_ignored()). The global init literally cannot be killed; if it ever exits, do_exit() panics the machine with “Attempted to kill init!” (Process Exit wait and Zombie Reaping covers that panic guard).
  • It is expected to loop on wait() forever. Because everything eventually reparents to PID 1, a correct init must continuously reap. This is the single most basic duty of The Role and Responsibilities of init.

A crucial subtlety the kernel handles for the default reaper: when a reparented orphan is already a zombie (it had exited before its parent did), reparent_leader() resets its exit_signal to SIGCHLD before notifying the new parent — the comment in the source is literally “We don’t want people slaying init” — so a cloned child that used an exotic termination signal cannot deliver that signal to PID 1 (kernel/exit.c v6.12).

The subreaper escape hatch

Dumping every orphan on PID 1 is wasteful for a process that wants to track its own descendants. Consider a service manager that supervises a daemon. Well-behaved daemons traditionally double-fork to detach from their launcher: the daemon forks, the intermediate parent exits immediately, and the grandchild — now orphaned — keeps running. Under the default rule, that grandchild reparents straight to PID 1, and when it dies PID 1 gets the SIGCHLD. The service manager never finds out, so it cannot restart the daemon or even know it died.

The subreaper solves this. A process calls prctl(PR_SET_CHILD_SUBREAPER, 1) to declare “I will act as init(1) for my descendants.” Per PR_SET_CHILD_SUBREAPER: “A subreaper fulfills the role of init(1) for its descendant processes. When a process becomes orphaned (i.e., its immediate parent terminates), then that process will be reparented to the nearest still living ancestor subreaper.” After such a reparent, “calls to getppid(2) in the orphaned process will now return the PID of the subreaper process”, and “when the orphan terminates, it is the subreaper process that will receive a SIGCHLD signal and will be able to wait(2) on the process to discover its termination status.”

The kernel implements this in find_new_reaper(), whose own comment states the three-tier policy verbatim (kernel/exit.c v6.12):

/*
 * When we die, we re-parent all our children, and try to:
 * 1. give them to another thread in our thread group, if such a member exists
 * 2. give it to the first ancestor process which prctl'd itself as a
 *    child_subreaper for its children (like a service manager)
 * 3. give it to the init process (PID 1) in our pid namespace
 */
static struct task_struct *find_new_reaper(struct task_struct *father,
				struct task_struct *child_reaper)
{
	struct task_struct *thread, *reaper;
 
	thread = find_alive_thread(father);
	if (thread)
		return thread;                 /* tier 1: surviving sibling thread */
 
	if (father->signal->has_child_subreaper) {
		unsigned int ns_level = task_pid(father)->level;
		for (reaper = father->real_parent;
		     task_pid(reaper)->level == ns_level;   /* stay in this PID namespace */
		     reaper = reaper->real_parent) {
			if (reaper == &init_task)
				break;
			if (!reaper->signal->is_child_subreaper)
				continue;          /* not a subreaper, keep walking up */
			thread = find_alive_thread(reaper);
			if (thread)
				return thread;     /* tier 2: nearest subreaper ancestor */
		}
	}
 
	return child_reaper;                       /* tier 3: PID 1 of the namespace */
}

Two distinct flags do the bookkeeping, and the distinction is the part that trips people up. is_child_subreaper is set on the process that called the prctl — it means “I am a subreaper.” has_child_subreaper is a propagated hint set on a process when it has some subreaper somewhere above it — it lets find_new_reaper skip the expensive ancestor walk entirely when no subreaper exists above the dying task. The prctl keeps the hint correct: case PR_SET_CHILD_SUBREAPER: me->signal->is_child_subreaper = !!arg2; if (!arg2) break; walk_process_tree(me, propagate_has_child_subreaper, NULL); (kernel/sys.c v6.12), which sets has_child_subreaper = 1 on every existing descendant (pruning subtrees that already have it). New descendants inherit the hint at fork: in copy_process(), p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper || p->real_parent->signal->is_child_subreaper; (kernel/fork.c v6.12). The deeper mechanics of this walk live in The Process Tree and Reparenting.

Two inheritance rules matter operationally, both per the man page and matching the code: the subreaper attribute is not inherited across fork/clone (each process must set its own), but it is preserved across execve — so a tiny wrapper can prctl itself a subreaper and then exec the real manager. It has existed since Linux 3.4 (2012). The man page’s own rationale names the use cases: “session management frameworks where a hierarchical group of processes is managed by a subreaper process that needs to be informed when one of the processes — for example, a double-forked daemon — terminates… Some init(1) frameworks (e.g., systemd(1)) employ a subreaper process for similar reasons.”

Uncertain

Verify: that systemd’s system PID-1 instance relies on PR_SET_CHILD_SUBREAPER rather than being the subreaper trivially by virtue of being PID 1 (it already is the namespace reaper). The clearer subreaper user is systemd --user and systemd-nspawn/container payloads that are not PID 1. Reason: the brief and the man page mention systemd generically; the exact internal call sites were not traced to systemd source in this pass. To resolve: grep systemd source for PR_SET_CHILD_SUBREAPER (it is used in the per-user manager and in nspawn’s stub init). uncertain

Worked example — why the subreaper changes the outcome

The classic double-fork that defeats a plain wait():

#include <sys/prctl.h>
#include <unistd.h>
 
/* (A) Without prctl: the grandchild's death goes to PID 1, not to us. */
if (fork() == 0) {                 /* child */
    if (fork() == 0) {             /* grandchild */
        /* ... real daemon work ... */
        _exit(0);
    }
    _exit(0);                      /* intermediate parent exits IMMEDIATELY */
}
wait(NULL);                        /* reaps only the intermediate child */
/* The grandchild was orphaned the instant its parent exited, and
   reparented away — to PID 1 in case (A). We never see its death. */
 
/* (B) With prctl(PR_SET_CHILD_SUBREAPER, 1) called BEFORE the forks,
   the orphaned grandchild reparents to US: getppid() in it returns our
   PID, and when it exits WE get the SIGCHLD and must wait() on it. */

In variant (A), the grandchild’s getppid() returns 1 after the intermediate parent exits; its death notification goes to system init. In variant (B), having declared ourselves a subreaper first, the grandchild’s getppid() returns our PID, and we receive the SIGCHLD — but only if we actually call wait() do we keep the process table clean. Declaring yourself a subreaper changes where deaths are reported, not whether reaping happens: a subreaper that never reaps is a zombie factory under its own subtree.

Failure Modes and Common Misunderstandings

“Orphans always go to PID 1.” This is the textbook answer and was true before Linux 3.4, but it is wrong on any modern system with subreapers. On a systemd machine an orphan may reparent to a systemd --user instance or a service scope, not to PID 1. Code that special-cases getppid() == 1 to mean “my parent died” is buggy in the presence of subreapers; use prctl(PR_SET_PDEATHSIG, …) or a pidfd instead.

“Reparenting reaps the orphan.” No. Reparenting only changes who is responsible; the orphan is still running. It becomes a zombie when it later exits, and the new parent must then wait() for it. Two separate events — the original parent dying (reparent) and the orphan later dying (zombie) — and the reaper has to act on the second.

The container PID-1 zombie problem (the headline failure). Inside a PID namespace, the application you launched is PID 1, and it inherits init’s reaping duty for the whole namespace — but most applications were never written to be init and do not reap. So every grandchild that double-forks, every subprocess whose immediate parent exits first, becomes an orphan that reparents to the application-as-PID-1, dies, and sits as a zombie that nothing collects. Over a long-running container these accumulate until the namespace (or, sharing the host’s table, the whole machine) runs out of PIDs. The fix is to run a proper init as PID 1 that does nothing but reap and forward signals.

PID 1’s silent signal dispositions (the other container PID-1 trap). As shown above, the kernel does not apply default signal actions to PID 1 — an unkillable task simply skips default-action signals. An application running as container PID 1 therefore ignores SIGTERM unless it explicitly installed a handler. docker stop sends SIGTERM, waits ~10s, then sends SIGKILL — so a naive PID-1 app appears to “ignore graceful shutdown” and is hard-killed every time, losing in-flight work. This is the second reason a real init shim is needed: it installs handlers and forwards signals to the real child.

Alternatives and When to Choose Them

PR_SET_PDEATHSIG is the dual of subreaping: a child asks to be signalled when its parent dies (prctl(PR_SET_PDEATHSIG, SIGTERM)), delivered at reparent time. Use it for a worker that must die with its supervisor — but mind the race (the parent may already be exiting; re-check getppid() after the call).

pidfd + waitid(P_PIDFD, …) (since Linux 5.4) gives a race-free file-descriptor handle to a specific process, pollable in an event loop, immune to PID reuse. For new code that needs to know when one specific process dies, this beats both SIGCHLD plumbing and PDEATHSIG. See Process Exit wait and Zombie Reaping.

A dedicated init shim as container PID 1 is the answer when the whole subtree must be reaped and signals forwarded. tini is the canonical minimal example: its README states it “protects you from software that accidentally creates zombie processes, which can (over time!) starve your entire system for PIDs (and make it unusable)” and that it “ensures that the default signal handlers work… SIGTERM properly terminates your process even if you didn’t explicitly install a signal handler for it” (tini README). It works by spawning a single child, then sitting in a loop “reaping zombies and performing signal forwarding.” Docker bundles tini and activates it with docker run --init (since Docker 1.13). dumb-init (Yelp) and the s6/runit supervisors solve the same problem. Note these shims are subtly different from a subreaper: a container PID 1 is the namespace reaper automatically (it is is_child_reaper), so it does not even need PR_SET_CHILD_SUBREAPER — its problem is purely that it must call wait() and forward signals, which a normal application fails to do.

Production Notes

The double-fork daemonization idiom exists partly because of reaping: by having the intermediate parent exit, the daemon is reparented to a reliable reaper (historically init, today often a subreaper like systemd) so the launching shell is freed of the obligation to wait. systemd itself leans on both mechanisms — a service’s processes live in a cgroup and under systemd’s reaping role, so even a daemon that double-forks to escape its parent is still caught (the cgroup membership is the authoritative grouping for systemd, independent of the process tree), and systemd applies its restart policy on the death notification. Build systems and CI runners (bazel, test harnesses) set themselves as subreapers so no descendant can escape cleanup by detaching, which would otherwise leak processes between builds. The recurring lesson across all of these: reparenting guarantees a responsible party exists, but that party still has to do the work of wait()-ing — and when the responsible party is an application accidentally promoted to PID 1, that work goes undone, which is the entire reason init shims exist.

See Also