Zombie vs orphan process
The classic interview trap, grounded: a <defunct> process in ps uses no CPU and no memory, and kill -9 does nothing — because a zombie is already dead. Anchor: KodeKloud (captured); verified against live kernel behavior below.
Zombie: exited child, unreaped
When a process exits, the kernel keeps one tiny entry in the process table holding its exit status, so the parent can read it. That read is reaping — the parent calls wait()/waitpid(). A zombie (state Z, <defunct>) is a child that already exited but whose parent hasn’t reaped it yet. No memory, no CPU — just the leftover slot. A brief zombie on every exit is normal; only a buildup is a problem (it can exhaust the PID table).
Why kill -9 does nothing: signals are for live processes. The fix is the parent — either it calls wait(), or it dies.
Orphan: alive child, dead parent
The opposite. An orphan is a child that’s still alive but whose parent exited first. Linux re-parents it to the nearest ancestor marked as a subreaper (PR_SET_CHILD_SUBREAPER via prctl()), falling back to PID 1 — systemd --user on a desktop, or the namespace’s PID 1 in a container. The adoptive parent reaps it later, when it exits. Orphans are harmless.
The life-cycle in one diagram
flowchart TD P["Parent process<br/>(must call wait())"] C["Child process"] Z["🧟 Zombie (state Z, defunct)<br/>exited, exit code held in process table<br/>no CPU, no memory"] R["Reaped ✅<br/>parent wait() reads exit code<br/>process-table slot freed"] A["🧒 Orphan (alive)<br/>re-parented to nearest subreaper<br/>PR_SET_CHILD_SUBREAPER, else PID 1<br/>adopted, reaped at its exit"] K["kill -9 parent (the fix for zombie buildup)<br/>zombies re-parent to init/subreaper<br/>which reaps them on the spot"] P -- fork() --> C C -- "exit() — kernel keeps<br/>slot + exit status" --> Z Z -- "parent wait()<br/>(reap)" --> R Z -- "parent never reaps<br/>(bug: buildup)" --> Z C -- "parent exits first<br/>(child still alive)" --> A P -- dies --> K K --> A
Container sharp edge: your app as PID 1
In containers, the app runs as PID 1 only with exec-form ENTRYPOINT/CMD (shell form leaves /bin/sh as PID 1). A normal app as PID 1 never reaps its grandchildren, and PID 1 also doesn’t get default signal handling — SIGTERM is ignored unless explicitly handled. The fixes: docker run --init (injects tini), or bake tini / dumb-init into the image — the portable option, since Kubernetes has no --init flag.
Interview framing
Hundreds/thousands of zombies is never a zombie problem — it’s a parent problem (a bug that isn’t reaping). Diagnosis order: identify the parent (ps -o ppid= -p <pid>), check whether it’s your code missing wait(), kill/restart the parent as the immediate clear, fix the reap logic as the real fix.
Kin
- State machines — process states (R/S/Z/D/T) as the OS’s own transition table; the zombie is a state with no exit transition until the parent acts
- Things I want to dive deeper — hands-on candidate: reproduce a zombie buildup in a lab (a C/Python parent that never waits), watch PID 1/subreaper adoption live
- Agent sandbox / container lanes — the tini/init-in-container rule applies to every containerized agent runtime