The Process Tree and Reparenting
Every task on a Linux system except the very first sits in a tree: it has exactly one parent, zero or more children, and a chain of ancestors leading back to PID 1. The kernel stores this structure directly in the
task_structas a parent pointer plus a sibling-linked list of children, and it maintains two parent pointers —real_parent(who created you) andparent(who gets yourSIGCHLDandwait), which differ only underptrace. The tree’s hardest invariant is what to do when a parent dies before its children: those children become orphans and must be reparented so that someone is always responsible for reaping them. Linux reparents an orphan to the nearest living ancestor that registered as a “child subreaper” (viaprctl(PR_SET_CHILD_SUBREAPER)), and failing that to PID 1 of its PID namespace — thechild_reaper(kernel/exit.cv6.12;PR_SET_CHILD_SUBREAPER(2const)). This note traces that machinery in the 6.12 LTS kernel (released 2024-11-17; the reparenting logic is byte-for-byte identical in 6.18 LTS, 2025-11-30 — only line numbers shift).
This note is the structure half of the lifecycle. How a task is born into the tree is fork clone and exec System Calls; how a task leaves it (becoming a zombie awaiting reap) is Process Exit wait and Zombie Reaping — and the reparenting code below runs inside that note’s exit_notify(). The numeric identities the tree is keyed on (PID, TGID, PGID, SID) are Process and Thread Identifiers in Linux.
Mental Model
Picture the process tree as a directed forest rooted at PID 1, where each edge is a real_parent pointer pointing up. The kernel keeps the down edges too — each parent owns a children list, and the children chain to each other through sibling links — so the tree can be walked in either direction. Reparenting is the operation that keeps the tree connected when an interior node is deleted: rather than orphaning a whole subtree, the kernel re-roots that subtree under a new parent, splicing the dead task’s children list onto the chosen reaper’s children list in a single move.
flowchart TB INIT["PID 1 (child_reaper)<br/>init / systemd"] SR["subreaper S<br/>prctl(PR_SET_CHILD_SUBREAPER)"] P["parent P (dies)"] C1["child C1"] C2["child C2"] INIT --> SR SR --> P P --> C1 P --> C2 P -. "P exits: C1,C2 reparented" .-> SR SR -. "to nearest subreaper ancestor" .-> C1 SR -. "(not all the way to PID 1)" .-> C2
Reparenting on parent death. What it shows: when parent P dies, its orphaned children C1/C2 are not dumped on PID 1 — the kernel walks up the real_parent chain and stops at the nearest ancestor that set the child-subreaper flag (S), reparenting the orphans to it. The insight to take: the subreaper flag lets a service manager (or a container’s PID 1) interpose itself as the responsible adoptive parent for an entire subtree, so it learns of the deaths of grandchildren and deeper descendants — including double-forked daemons that deliberately detach — instead of those deaths silently going to system init. Absent any subreaper, the chain terminates at PID 1.
Mechanical Walk-through
The tree fields in task_struct
The hierarchy is four fields in include/linux/sched.h (v6.12, lines 1031–1051):
struct task_struct __rcu *real_parent; /* who created us */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children; /* list of our natural children */
struct list_head sibling; /* our link in real_parent->children */
struct list_head ptraced; /* tasks we are ptracing */
struct list_head ptrace_entry; /* our link in tracer->ptraced */real_parent and parent are normally the same task. They diverge under ptrace: when a debugger attaches to a task, the kernel sets the tracee’s parent to the tracer (so stop/continue notifications and wait go to the debugger) while keeping real_parent pointing at the original creator. getppid(2) returns the real_parent’s PID, so ptrace does not lie to the child about its lineage. Both pointers are __rcu-annotated because the lock-free walkers (/proc, the OOM killer, pgrp scans) traverse them under rcu_read_lock() while holders mutate them under the tasklist_lock writer lock. The children/sibling pair forms an intrusive doubly-linked list: a parent enumerates its children with list_for_each_entry(p, &father->children, sibling).
A task is spliced into the tree at fork time (in copy_process, covered in fork clone and exec System Calls): the new task’s real_parent/parent are set, and it is added to the parent’s children list. It is spliced out at exit, and — if it had children of its own — those children are reparented, which is the subject of the rest of this note.
forget_original_parent() — re-rooting the orphans
When a task dies, exit_notify() calls forget_original_parent(father, &dead) (v6.12 line 692) under write_lock_irq(&tasklist_lock). Its job, per its own comment, is “(A) make init inherit all the child processes” and “(B) check for newly orphaned process groups.” It proceeds in three steps:
- Pick the reaper. It first calls
find_child_reaper()(line 587) to resolve PID 1 of this task’s PID namespace, thenfind_new_reaper()(line 625) to refine that to the nearest subreaper if one exists. (Detail below.) - Re-point every child. For each child
pinfather->children, and for each threadtof that child, it setsRCU_INIT_POINTER(t->real_parent, reaper)and (when not ptraced)t->parent = t->real_parent(lines 706–711). If the child had requested a parent-death signal viaprctl(PR_SET_PDEATHSIG), that signalt->pdeath_signalis delivered now (line 712) — this is how a child can be notified that its parent died. - Splice the lists. Finally
list_splice_tail_init(&father->children, &reaper->children)(line 724) moves the entire children list onto the reaper in O(1), rather than re-linking each child individually.
For each reparented child that is already a zombie, reparent_leader() (line 663) calls do_notify_parent() so the new parent gets the SIGCHLD and can reap it — and it resets the orphan’s exit_signal to SIGCHLD (/* We don't want people slaying init. */, line 670) so a clone child that used an exotic termination signal does not deliver that to init.
find_new_reaper() — the subreaper search
find_new_reaper(father, child_reaper) (line 625) encodes the three-tier policy spelled out in its comment verbatim:
- give them to another thread in our thread group, if such a member exists
- give it to the first ancestor process which prctl’d itself as a child_subreaper for its children (like a service manager)
- give it to the init process (PID 1) in our pid namespace
Step 1 is find_alive_thread(father) — if the dying task is one thread of a multithreaded process and a sibling thread is still alive, the children stay within the same process (a threaded reparent, which needs no notification because nothing has logically died). Step 2 runs only if father->signal->has_child_subreaper is set, and walks up the real_parent chain looking for an ancestor with is_child_subreaper set, stopping at &init_task or when the PID-namespace level changes (lines 644–654). Step 3 is the fallback: return child_reaper, i.e. PID 1 of the namespace.
The two flags are distinct and this is the subtle part. is_child_subreaper is set on the process that called prctl(PR_SET_CHILD_SUBREAPER, 1) — it marks “I am a subreaper.” has_child_subreaper is a propagated hint set on a process when it has some subreaper ancestor — it is the fast-path test find_new_reaper checks first to avoid walking the chain when there is no subreaper above at all. prctl keeps has_child_subreaper correct by calling walk_process_tree(me, propagate_has_child_subreaper, NULL) (kernel/sys.c line 2613), which sets has_child_subreaper = 1 on every existing descendant and prunes subtrees that already have it (sys.c v6.12 lines 2298–2313); new descendants inherit the flag at fork.
find_child_reaper() — PID 1 of the namespace, and the empty-namespace case
find_child_reaper(father, dead) (line 587) resolves the namespace’s reaper: pid_ns->child_reaper, the task that is PID 1 inside this task’s PID namespace. The key insight is that “init” is per-namespace: a process inside a container’s PID namespace sees its container’s PID 1 as init, and orphans there reparent to that PID 1, never to the host’s PID 1 (Process and Thread Identifiers in Linux covers PID namespaces). If the dying task is the namespace’s PID 1 and has no other live thread, find_child_reaper calls zap_pid_ns_processes(pid_ns) (line 612): the death of a PID-namespace init kills every remaining process in that namespace, because there is no longer anyone to reap them — this is why killing a container’s PID 1 tears down the whole container.
Process groups and sessions — the orphaned-pgrp rule
The tree is not the only grouping the kernel tracks. Each process belongs to a process group (identified by a PGID, normally the PID of the group leader) and each process group belongs to a session (identified by a SID, normally the PID of the session leader). These are the substrate of shell job control: a pipeline a | b | c is one process group; foreground/background and terminal signal delivery (Ctrl-C → SIGINT to the foreground group) operate per-group; a login shell is a session leader with a controlling terminal. The numbers themselves (PGID/SID) and the setpgid(2)/setsid(2) system calls that manipulate them live in Process and Thread Identifiers in Linux; what matters here is the reparenting-adjacent rule the exit path enforces.
When a process exits, exit_notify (via kill_orphaned_pgrp, line 374) checks whether its death newly orphans a process group. A process group is “orphaned” (POSIX 2.2.2.52) when no member has a parent in a different group of the same session — i.e. the group has lost its connection to the job-control shell that was managing it. If a newly orphaned group contains stopped jobs, the kernel sends the whole group SIGHUP followed by SIGCONT (lines 395–396), so stopped processes are not left frozen forever with no shell to resume them. This is a job-control safety valve woven into the same exit path as reparenting, which is why it lives in kernel/exit.c next to the tree code.
Code and Configuration
Registering as a subreaper is a one-line prctl, per PR_SET_CHILD_SUBREAPER(2const):
#include <sys/prctl.h>
/* "I will adopt and reap any of my descendants that get orphaned." */
prctl(PR_SET_CHILD_SUBREAPER, 1); /* set; 0 to unset */
int is_subreaper;
prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper); /* query: writes 0/1 via PR_GET */The man page is precise about the semantics this unlocks: “A subreaper fulfills the role of init(1) for its descendant processes. When a process becomes orphaned … it will be reparented to the nearest still living ancestor subreaper. Subsequently, calls to getppid(2) in the orphaned process will now return the PID of the subreaper, and when the orphan terminates, it is the subreaper that will receive a SIGCHLD and will be able to wait(2) on it.” Two inheritance rules from the same page (and matching the sys.c code): the attribute is not inherited across fork/clone (each process must set its own), but it is preserved across execve (so a wrapper can set it and then exec the real service manager). It was introduced in Linux 3.4.
A minimal demonstration of why a subreaper matters — the double-fork that defeats a plain wait:
/* Without a subreaper, this 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. If WE called prctl(PR_SET_CHILD_SUBREAPER,1) first,
the grandchild reparents to US and we get its SIGCHLD. */This is exactly the case the PR_SET_CHILD_SUBREAPER page names: “session management frameworks … managed by a subreaper that needs to be informed when one of the processes — for example, a double-forked daemon — terminates (perhaps so it can restart it). Some init(1) frameworks (e.g. systemd) employ a subreaper process for similar reasons.”
Failure Modes and Common Misunderstandings
“Orphans always go to PID 1.” This was true before Linux 3.4 and is the textbook answer, but it is now wrong on any system with subreapers. On a modern systemd machine, an orphan reparents to its systemd --user instance or a service’s systemd scope, not to PID 1; getppid() returning something other than 1 after the real parent died is the tell. Code that special-cases getppid() == 1 to detect “my parent died” is buggy in the presence of subreapers — use prctl(PR_SET_PDEATHSIG) or a pidfd instead.
“A subreaper reaps; it does not need to call wait.” False — becoming a subreaper only makes orphans reparent to you and sends you their SIGCHLD; you still have to actually call wait/waitid or those orphans accumulate as zombies under you (see Process Exit wait and Zombie Reaping). A subreaper that forgets to reap is a zombie factory.
Container PID 1 that ignores reaping. Inside a PID namespace, the application running as PID 1 inherits init’s reaping duty for the whole namespace. If it does not reap, zombies pile up in the container; if it dies, zap_pid_ns_processes kills everything in the namespace. This is why container images run a tiny init shim (tini, dumb-init, Docker --init) whose sole job is to be a correct reaping, signal-forwarding PID 1.
Confusing parent and real_parent. Tools that read /proc/PID/stat’s parent field see the real_parent; a debugger attached via ptrace becomes the parent for wait/stop purposes but is invisible to getppid. Assuming the two are always equal breaks under any tracer (including strace, gdb).
Alternatives and Related Mechanisms
PR_SET_PDEATHSIG is the dual of subreaping: instead of the parent arranging to be told of a child’s death, a child arranges to be sent a signal when its parent dies (prctl(PR_SET_PDEATHSIG, SIGTERM)). It is delivered in forget_original_parent (line 712) at reparent time. Use it for a worker that should die with its supervisor. Note its well-known race: the parent may already be exiting when you set it, so re-check getppid() after the call.
pidfd (see Process and Thread Identifiers in Linux and fork clone and exec System Calls) gives a race-free handle to a specific process for death notification via poll, independent of the tree and of PID reuse — increasingly the preferred mechanism over both SIGCHLD and PDEATHSIG for new code.
PID namespaces provide per-namespace reparenting roots: the child_reaper is per-namespace, so a container’s PID 1 is the reaping root for everything inside it, transparently. This is the kernel feature that makes a container’s process tree a self-contained subtree.
Production Notes
systemd leans hard on subreaping: a service’s processes run in a cgroup and under systemd’s subreaper role, so even a daemon that double-forks to escape its parent is still caught by systemd (which knows the process belongs to the service’s cgroup regardless of the tree), and systemd can apply its restart policy on the death SIGCHLD. The cgroup membership is the authoritative grouping for resource control and is independent of the process tree — the tree tells you lineage, the cgroup tells you accounting/limits (The cgroup v2 CPU Controller). Build systems and test harnesses (e.g. bazel, CI runners) also set themselves as subreapers so that no descendant process can escape cleanup by detaching, which would otherwise leak processes between builds.
See Also
- Process Exit wait and Zombie Reaping — the exit path that runs this reparenting code (
exit_notify→forget_original_parent); zombies,SIGCHLD, andwait - Process and Thread Identifiers in Linux — PID/TGID/PGID/SID, PID namespaces,
setpgid/setsid, and PID reuse the tree is keyed on - fork clone and exec System Calls — how a task is spliced into the tree at creation;
CLONE_PARENT,PR_SET_PDEATHSIGsetup - The task_struct Process Descriptor — where
real_parent,parent,children,siblingphysically live - Threads as Tasks and the CLONE Flags — why a threaded reparent (a surviving sibling thread) needs no notification
- Linux Process Scheduling MOC — parent MOC, §A The Task and Its Lifecycle