Process lifecycle
Every Linux process is born via fork() (clone the parent) then exec() (replace with a new program). The parent must call wait() to reap the child; a child whose parent never waits becomes a zombie. Orphans are reparented to PID 1.
You’ve used ps aux to list processes and kill to stop them. But where does a process come from? Every process on your system — from sshd to nginx to the shell you type in — was born the same way: the kernel was asked to clone an existing process, then replace its program image. Understanding this two-step birth (fork + exec) explains why zombie processes appear, why orphans get adopted by PID 1, and what the process state letters in ps output actually mean. It also previews the mechanism containers use to create isolated process trees.
After this lesson you can explain the fork/exec/wait lifecycle, read process state flags in ps, explain what a zombie is and why it is not dangerous by itself, explain what an orphan is and where it ends up, and understand why a PID namespace gives a container its own PID 1.
Every new process is born from fork() — a clone of the parent.
fork() is a system call that creates an exact copy of the calling process. The child gets:
- A new PID
- A copy-on-write copy of the parent’s memory
- Copies of all open file descriptors
- The same program counter (both parent and child resume from after
fork())
fork() returns twice: it returns the child’s PID in the parent, and 0 in the child. This is how both processes know which role they play.
# Observe fork in action: your shell forks for every command
# Before the fork: one bash process
echo $$ # print the current shell's PID, e.g. 1234
# The shell forks, then the child execs 'ls'
ls /tmp
# After ls exits, control returns to the parent (bash)
# The child process no longer existsThe key insight: the child is not a blank slate — it is a clone. That clone-first, replace-second model is why spawning a child is cheap: only pages that are actually written get copied (copy-on-write).
exec() replaces the child’s program image with a new one.
After fork(), the child typically calls one of the exec() family of syscalls (execve, execvp, etc.). This replaces the child’s memory, code, and stack with a new program. File descriptors are inherited unless marked close-on-exec.
# strace lets you see the fork+exec sequence for any command
strace -e trace=execve ls /tmp 2>&1 | head -5
# execve("/usr/bin/ls", ["ls", "/tmp"], 0x... /* env */) = 0
# The kernel replaces the process image with /usr/bin/ls
# The return value 0 means success
# You can also see it for a shell command:
strace -e trace=clone,execve bash -c 'echo hello' 2>&1 | grep -E 'clone|execve'
# clone(...) = <child-pid> -- this is the fork()
# execve("/usr/bin/bash", [...]) -- child execs bash with -c echo helloThe shell does this thousands of times a day. Every pipeline stage, every subshell, every $(command) substitution — each is a fork followed by an exec.
Process states: what the STAT column in ps really means.
Every process is in one of these kernel scheduler states at any moment:
ps aux
# USER PID %CPU %MEM VSZ RSS TTY STAT ...
# root 1 0.0 0.2 22596 9320 ? Ss ... ← systemd: sleeping, session leader
# www-data 987 0.1 1.1 123456 44000 ? S ... ← nginx worker: interruptible sleep
# root 123 0.0 0.0 0 0 ? D ... ← in uninterruptible I/O wait
# root 3456 0.0 0.0 0 0 ? Z ... ← zombie: exited, not yet reaped
# STAT letter meanings:
# R Running or runnable (on the run queue or actually on a CPU)
# S Interruptible sleep (waiting for an event; can be woken by a signal)
# D Uninterruptible sleep (waiting for I/O; CANNOT be killed — not even by SIGKILL)
# Z Zombie (exited but parent has not called wait() yet)
# T Stopped (SIGSTOP, Ctrl-Z, or being traced by a debugger)
# + Foreground process group (shown as S+ or R+)
# s Session leader
# l Multi-threadedThe D state is the one that surprises operators: a process stuck in D state does not respond to kill -9. It is inside a kernel operation (usually an I/O syscall) and the kernel will not interrupt it. If a process is stuck in D for minutes, the problem is the underlying I/O (hung NFS mount, dying disk) not the process itself.
wait(): the parent must reap the child, or it becomes a zombie.
When a process exits, the kernel keeps a small record (the exit status, the PID, resource usage). This record stays until the parent calls wait() or waitpid() to collect it. A child that has exited but not been reaped is a zombie (state Z).
# Zombies show up in ps as:
# 1234 Z+ 0:00 [defunct]
# How many zombies do you have right now?
ps aux | awk '$8 ~ /Z/ { print $0 }'
# Zombies are NOT dangerous by themselves:
# They consume a PID slot and a small kernel struct — nothing else.
# No CPU, no memory, no file descriptors.
# Zombies DO matter if there are thousands of them:
# PIDs are a finite resource (cat /proc/sys/kernel/pid_max — usually 4194304)
# A parent with a bug that never calls wait() accumulates zombies over time.
# The fix is to fix the parent (or restart it), not to kill the zombies.
# Finding the parent of a zombie:
# Get the zombie's PID (e.g. 1234), then:
ps -o ppid= -p 1234 # prints the parent PID
# That parent is the one that needs to call wait()Orphans and PID namespaces: what happens when a parent dies first.
If a parent exits while its children are still running, those children become orphans. The kernel immediately reparents orphans to PID 1 (systemd). PID 1 continuously calls wait() in a loop, so orphan zombies are immediately reaped.
# Demonstrate orphan adoption:
# In one terminal, start a process that spawns a child and exits:
bash -c 'sleep 300 & echo "child PID: $!"' &
# Now kill the parent bash immediately
# The sleep 300 continues running — check its parent:
sleep 1
pstree -p | grep sleep
# The sleep is now a child of PID 1 (systemd), not of the original bash
# PID namespace preview (container bridge):
# A container is (among other things) a new PID namespace.
# Inside the namespace, processes see PIDs starting from 1.
# The first process in that namespace becomes its PID 1.
# If that process exits without reaping its children, zombies accumulate.
# This is why container PID 1 must handle signals and call wait() — or use a tiny init.
unshare --pid --fork --mount-proc bash
# Inside: ps shows PID 1 = bash, PID 2 = ps
# Exit: all processes in the namespace are killedDiagnosing a service that accumulates zombie children.
Situation: you notice a Node.js app server has dozens of [defunct] processes in ps aux. The monitoring shows PID count trending up. Here is the full diagnostic and resolution flow:
# Step 1: confirm zombies and find the guilty parent
ps aux | awk '$8 ~ /Z/'
# USER PID %CPU %MEM VSZ RSS TTY STAT COMMAND
# node 4201 0.0 0.0 0 0 ? Z [node]
# node 4202 0.0 0.0 0 0 ? Z [node]
# ... (dozens)
# Get the parent of zombie PID 4201:
ps -o ppid= -p 4201
# 3890
# Step 2: what is PID 3890?
ps -p 3890 -o pid,comm,args
# 3890 node node /app/server.js
# Step 3: verify the parent is not calling wait()
# (a well-behaved parent would have 0 zombies)
# Look at how many zombie children this parent has:
ps --ppid 3890 -o pid,stat | awk '$2 ~ /Z/'
# Step 4: resolution options (in order of preference):
# a) Fix the application: ensure every child.on('exit') or
# cluster 'exit' event properly handles the exit — Node's
# child_process module requires listening to the 'exit' or 'close'
# event or the child object is never GC'd properly.
# b) Restart the Node process: all its zombie children will then
# be reparented to PID 1, which reaps them immediately.
# c) If this is a third-party service: open a bug report.
# Zombie accumulation is always a parent bug, not a zombie bug.
# Step 5: verify PID headroom is not critically low
cat /proc/sys/kernel/pid_max # usually 4194304
ps aux | wc -l # current process count
# If current count approaches pid_max, you have a real resource exhaustion problem▸Why this works
On macOS, the fork/exec model is the same (it is POSIX), but ps uses BSD flags. The STAT column shows Z for zombies identically. The significant macOS difference: launchd plays the role of PID 1 and the zombie reaper. There is no /proc pseudo-filesystem — process information is accessed through sysctl() calls and tools like lsof, dtrace, and Activity Monitor. The concepts of fork/exec/wait, zombie, and orphan are universal; only the tooling differs.
▸Common mistake
Killing a zombie with kill -9 does nothing. The zombie is already dead — it is just a kernel record waiting for wait(). SIGKILL goes to a living process. A zombie has no running code, no signal handler, nothing to receive the signal. The only entity that can make a zombie go away is its parent (by calling wait()), or PID 1 (by the parent dying and orphaning the zombies, after which PID 1 reaps them). If you want the zombies gone immediately, kill the parent, not the zombies.
A process shows state 'D' in ps aux STAT column for 10 minutes. What does this mean, and will kill -9 stop it?
Every Linux process is born via fork() (the kernel clones the parent) followed by exec() (the child replaces its program image). Process states in ps reflect the kernel scheduler: R (running), S (interruptible sleep), D (uninterruptible I/O sleep — immune to SIGKILL), Z (zombie — exited, not yet reaped), T (stopped). A zombie is an already-exited child whose parent has not yet called wait() to collect its exit status; zombies consume a PID slot but no CPU or memory, and they cannot be killed because they are already dead. An orphan is a running child whose parent has exited; the kernel immediately reparents orphans to PID 1, which continuously reaps them. PID namespaces extend this model: a container’s first process becomes PID 1 inside its namespace, responsible for reaping children in that namespace — which is why containers need a proper init or a signal-aware entrypoint.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.