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