Linux · lesson 3 of 7
Beginner
2Intermediate
5- 2 Permission Bits, Ownership, and Why passwd Needs setuid
- 3 A Zombie Process That Never Gets Reaped — and the PID 1 That Fixes It
- 4 systemd Service Supervision: Restart Limits and Why They Trip
- 5 Linux Networking: Routes, Interfaces, and Bind Addresses
- 7 Inodes: The Filesystem Is Full, but df -h Says 1% Used
Also part of Linux
7- + Bash Text Processing from Bash
- + Bash Process Management from Bash
- + Bash System Information from Bash
- + Cron and Scheduling in Bash from Bash
- + Bash File Operations from Bash
- + Reading Linux SSH Auth Logs: Failed Logins You Generate Yourself from Defensive Security
- + Host Firewalls with nftables: DROP Times Out, REJECT Refuses from Networking
Also part of: Bash
A Zombie Process That Never Gets Reaped — and the PID 1 That Fixes It
A child exits, its parent never calls wait(), and the zombie sits forever under a naive PID 1 — orphaned to PID 1 doesn't help unless that PID 1 actually reaps. Docker's --init does; a bare sleep infinity doesn't.
By SkillByExample · Updated
Killing an orphaned zombie’s new parent should clean it up — except when that new parent never does the one thing that would.
Creating a real zombie
// zombie_maker.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("child (pid %d) exiting now\n", getpid());
exit(0);
} else {
printf("parent (pid %d) sleeping without wait()\n", getpid());
sleep(30);
}
return 0;
}
docker run -d --name linux-lesson --privileged ubuntu:22.04 sleep infinity
gcc -o zombie_maker zombie_maker.c
docker exec -d linux-lesson /root/zombie_maker
docker exec linux-lesson ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2792 1008 ? Ss 05:55 0:00 sleep infinity
root 1883 0.0 0.0 2776 956 ? Ss 06:07 0:00 /root/zombie_maker
root 1889 0.0 0.0 0 0 ? Z 06:07 0:00 [zombie_maker] <defunct>
root 1890 0.0 0.0 7072 1560 ? Rs 06:07 0:00 ps aux
STAT Z and <defunct> — a real zombie. The child (1889) already exited; the parent
(1883) is 30 seconds into a sleep() and hasn’t called wait() yet, so the kernel keeps the
child’s exit status parked in the process table.
docker exec linux-lesson cat /proc/1889/status
Name: zombie_maker
State: Z (zombie)
Tgid: 1889
Pid: 1889
PPid: 1883
...
/proc/<pid>/status confirms it directly: State: Z (zombie), and PPid: 1883 names the
exact parent responsible for reaping it.
The bug: orphaning doesn’t guarantee reaping
docker exec linux-lesson kill -9 1883
docker exec linux-lesson ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2792 1008 ? Ss 05:55 0:00 sleep infinity
root 1889 0.0 0.0 0 0 ? Z 06:07 0:00 [zombie_maker] <defunct>
root 1908 0.0 0.0 7072 1612 ? Rs 06:08 0:00 ps aux
The parent is gone — but the zombie is still there.
docker exec linux-lesson cat /proc/1889/status
Name: zombie_maker
State: Z (zombie)
Pid: 1889
PPid: 1
...
PPid: 1 confirms the zombie was correctly re-parented (“orphaned”) to this container’s
PID 1 — but that PID 1 is a plain sleep infinity, whose entire implementation is “sleep.”
It never calls wait() on anything, adopted children included, because nothing in it does
that. Orphaning moves responsibility for reaping to PID 1; it does not guarantee PID 1 will
actually do it.
The fix: a real init as PID 1
docker run -d --name zombie-test --init ubuntu:22.04 sleep infinity
docker exec zombie-test ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 4.0 0.0 1008 4 ? Ss 06:08 0:00 /sbin/docker-init -- sleep infinity
root 7 0.0 0.0 2792 1020 ? S 06:08 0:00 sleep infinity
root 8 0.0 0.0 7072 1592 ? Rs 06:08 0:00 ps aux
Docker’s --init flag inserts docker-init (a build of tini) as the real PID 1, with the
specified command (sleep infinity) running as its child instead. Repeat the exact same
zombie-creation steps against this container:
docker exec -d zombie-test /root/zombie_maker
docker exec zombie-test kill -9 878 # kill the zombie_maker parent
docker exec zombie-test ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 1008 4 ? Ss 06:08 0:00 /sbin/docker-init -- sleep infinity
root 7 0.0 0.0 2792 1020 ? S 06:08 0:00 sleep infinity
root 897 2.0 0.0 7072 1608 ? Rs 06:10 0:00 ps aux
No zombie anywhere in the list. tini’s entire purpose is implementing the two
responsibilities a real PID 1 needs — reaping orphaned children and forwarding signals — which
is exactly the gap between “something is running as PID 1” and “PID 1 actually does PID 1’s
job.”
Practice
1. Confirm the zombie under the naive sleep infinity PID 1 never goes away on its own.
docker exec linux-lesson ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2792 1008 ? Ss 05:55 0:00 sleep infinity
root 1889 0.0 0.0 0 0 ? Z 06:07 0:00 [zombie_maker] <defunct>
Still there, unchanged, well after the original 30-second sleep would have elapsed — this
zombie is permanent until the container itself is removed, since nothing in this container
will ever call wait() on it.
2. Check whether the reaped zombie's PID gets reused.
docker exec zombie-test ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 1008 4 ? Ss 06:08 0:00 /sbin/docker-init -- sleep infinity
root 7 0.0 0.0 2792 1020 ? S 06:08 0:00 sleep infinity
Once reaped, the zombie’s process table entry is fully freed, and the kernel is free to reissue that PID number to a future process — this is exactly why an unreaped zombie is a genuine resource leak at scale: every zombie holds a PID that can’t be reused until it’s collected.
3. List every process in the D (uninterruptible sleep) or Z (zombie) state on a system, the two states worth actually worrying about.
docker exec zombie-test ps -eo pid,stat,comm --sort=stat
PID STAT COMMAND
902 Rs ps
1 Ss docker-init
7 S sleep
No D or Z entries here — this is the actual command worth running on a real system when
investigating “is something stuck”: D means a process is blocked on uninterruptible I/O
(often a hung disk or NFS mount), Z means an unreaped zombie. Both are visible at a glance
by sorting on the STAT column instead of scanning a full ps aux by eye.
Next: systemd and service management — how a real init system supervises long-running services, restarts them on crash, and orders startup dependencies.