Module 02 — Processes, fork/exec & Process States

Updated 21 August 2026

Module 02 · Processes — how they are born, how they die

Most Linux problems you will ever debug are process problems. This module shows you where processes come from, how they end, and what those odd letters in ps actually mean.

🧠 concept → 🧩 real-world analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start, you should already know (all from Module 01): what the kernel is, what user space and kernel space mean, what a system call is, how to use strace, and how to read files under /proc.

If any of those is fuzzy, go back. This module builds directly on them.


🧱 Part A · What a process is made of

A1 · What the kernel remembers about each process

In Module 01 you learned that a process is a running copy of a program.

To manage a process, the kernel has to remember things about it. It keeps a set of notes for every single process on the machine. Textbooks call these notes the Process Control Block. The name does not matter. What matters is what is written on it.

Here is what the kernel writes down:

What it remembersWhy it needs itWhere you can see it
Its number (PID)So everything else can refer to this process/proc/PID/status
Its parent (PPID)So it knows who to report to when the process dies/proc/PID/status
Its stateSo the kernel knows whether to give it CPU timeps STAT column
Where it stoppedSo it can carry on from the same place after being pausedNot visible — CPU registers
Its memory mapSo it can never touch another process's memory/proc/PID/maps
Its open filesSo read and write know what you meant/proc/PID/fd
Which user it runs asSo the kernel can decide what it is allowed to do/proc/PID/status
CPU time usedSo the scheduler can share fairlyps TIME column

This is why /proc/PID/ had all those files in Module 01. It is the kernel's notes, opened up for you to read.

Real-world analogy — the chart at the end of a hospital bed

Every patient in a hospital has a chart. It has their ID number, who admitted them, how they are doing right now, what they are on, and what has been done so far.

The chart is not the patient. It is what the hospital needs to know in order to look after them. Nurses change shift, doctors come and go, but the chart stays and anyone can pick it up and carry on.

The kernel's notes work the same way. Your process gets moved off the CPU hundreds of times a second, and each time the kernel needs the chart to pick up where it left off.

Where the analogy stops working. A hospital chart is written by people, slowly. The kernel's notes are updated constantly, by the kernel itself, and you are only ever allowed to read them. You cannot pick up the chart and change the patient's blood type.

🧪 Exercise A1.1 — Read the kernel's notes about your own shell
bash
# $$ is your shell's own PID
echo "My shell is PID $$"

# The kernel's notes, in a form humans can read
grep -E '^(Name|State|Pid|PPid|Uid|Gid|Threads|VmRSS)' /proc/$$/status

# How many separate things is the kernel remembering here?
wc -l < /proc/$$/status
Expected result — click to reveal
plain text
$ echo "My shell is PID $$"
My shell is PID 3901

$ grep -E '^(Name|State|Pid|PPid|Uid|Gid|Threads|VmRSS)' /proc/$$/status
Name:	bash
State:	S (sleeping)
Pid:	3901
PPid:	3898
Uid:	1000	1000	1000	1000
Gid:	1000	1000	1000	1000
VmRSS:	   5504 kB
Threads:	1

$ wc -l < /proc/$$/status
58

What to read out of it.

Name: bash — the program this process is running.

State: S (sleeping) — your shell is asleep. It is waiting for you to type. It is using no CPU at all. This is normal for almost every process, as you saw in Module 01.

Pid: 3901 and PPid: 3898 — this process has a number, and it has a parent. Section A2 is all about that second number.

Uid has four numbers, not one. They are not all the same thing. Section A3 explains why that matters.

58 lines. The kernel is tracking 58 separate facts about one idle shell. Multiply that by the 243 processes from Module 01 and you can see why process management is a real job and not an afterthought.

Now imagine this at 500 hosts. Every monitoring tool that shows you "top processes by memory" is reading exactly this file, for every process, on a timer. That is why a host running 30,000 processes can spend real CPU just being monitored.

A2 · PIDs, parents, and the process tree

Two simple rules explain the whole shape of a Linux system:

  1. Every process has a number. That is the PID.
  2. Every process was started by another process. The starter is the parent, and its number is the PPID.

Rule 2 has exactly one exception. PID 1 is started by the kernel itself when the machine boots. It has no parent. Everything else on the machine is a descendant of PID 1.

So processes are not a flat list. They are a tree.

Diagram source
flowchart TD
    A["PID 1 - systemd<br>started by the kernel"]
    B["sshd"]
    C["cron"]
    D["nginx"]
    E["your login session"]
    F["bash - your shell"]
    G["the command you just ran"]
    A --> B
    A --> C
    A --> D
    B --> E
    E --> F
    F --> G
Real-world analogy — who hired whom

Picture a company org chart. Every employee was hired by somebody. Follow the arrows up from anyone and you always end at the founder — the one person nobody hired.

PID 1 is the founder. Your shell was hired by your login session, which was hired by sshd, which was hired by PID 1.

The useful part is what happens when a manager leaves. The company does not sack their whole team — the team gets reassigned to someone else. Linux does exactly this, and Section C5 shows you it happening live.

Where the analogy stops working. In a company you can be transferred to a new manager many times. In Linux there is only ever one reassignment, and it always goes to the same place: PID 1. There is no middle management.

A small thing that confuses people. PID numbers are handed out in order and then start again from the beginning once they run out. So a PID is not a permanent name — it is more like a table number in a restaurant. The number gets reused after the process is gone.

This is a real source of bugs. A script that saves a PID, waits, and then runs kill on it can end up killing a completely different process that happens to have been given the same number. This is why proper tools track processes by cgroup or by a PID file with a lock, rather than by a bare number.

🧪 Exercise A2.1 — Walk up your own family tree
bash
# Who am I, and who started me?
ps -o pid,ppid,comm -p $$

# The whole chain, from PID 1 down to your shell
pstree -ps $$

# PID 1 itself - note it has no parent
ps -o pid,ppid,comm -p 1

# The highest PID number this machine will use before wrapping around
cat /proc/sys/kernel/pid_max
Expected result — click to reveal
plain text
$ ps -o pid,ppid,comm -p $$
    PID    PPID COMMAND
   3901    3898 bash

$ pstree -ps $$
systemd(1)---sshd(812)---sshd(3893)---sshd(3898)---bash(3901)---pstree(4402)

$ ps -o pid,ppid,comm -p 1
    PID    PPID COMMAND
      1       0 systemd

$ cat /proc/sys/kernel/pid_max
4194304

What to read out of it.

The pstree line is the best thing on this page. Read it left to right and you can see your entire login story: the kernel started systemd, which started sshd, which accepted your connection, which started your shell, which started pstree so it could print this.

Notice pstree(4402) on the end. The command you ran to look at the tree is itself in the tree. There is no outside.

ps -o pid,ppid,comm -p 1 shows PPID 0. There is no process 0 — that is just how the kernel says "no parent". PID 1 is the root.

pid_max is 4,194,304 here. Once PIDs reach that number they wrap back around to the beginning and start reusing free numbers. On older systems the default was only 32,768, which wrapped in hours on a busy box.

Now imagine this at 500 hosts. pstree -ps <pid> is the fastest way to answer "what started this thing?" when you find a process you do not recognise. A crypto-miner started from a web server shows up instantly, because its parent chain runs back through the web server rather than through systemd.

A3 · Which user a process runs as

The kernel decides what a process may do by looking at which user it is running as. Not which user started it. Which user it is running as right now.

Those are usually the same. Sometimes they are not, and that gap is worth understanding.

There are two numbers you need:

  • Real UID — who you actually are. It comes from your login and normally never changes.
  • Effective UID — who the kernel treats you as when checking permissions. This is the one that counts.

Almost always these match. But a program file can carry a special flag called setuid. When you run a setuid program, its effective UID becomes the file owner's UID instead of yours.

Real-world analogy — your staff card and a visitor badge

You work in a building and you have a staff card. It has your name on it and it opens the doors you are allowed through. That is your real UID — who you are.

Now you need something from the secure store room, which your card does not open. Reception does not give you a new identity. They give you a temporary badge that opens that one door, for that one job. Security scans the badge, not your face. That badge is your effective UID.

You are still you. Your name has not changed. But for the purposes of that door, you count as someone else.

This is exactly how passwd works. Changing your password means writing to a file only root can write to. So passwd is owned by root and carries the setuid flag. While it runs, it counts as root — but only that program, and only while it runs.

Where the analogy stops working, and why it matters. A visitor badge opens one door. A setuid-root program has all of root's powers for as long as it runs, not just the one it needs. That is why a bug in a setuid program is so dangerous, and why the number of setuid programs on a system is something security teams count.

🧪 Exercise A3.1 — Find a program that changes who you are
bash
# Who are you?
id

# Real and effective UID for your own shell - these should match
ps -o pid,ruser,euser,comm -p $$

# Now look at passwd. Notice the 's' where you would expect an 'x'.
ls -l /usr/bin/passwd

# How many setuid programs are on this machine?
find /usr/bin /usr/sbin -perm -4000 -type f 2>/dev/null
Expected result — click to reveal
plain text
$ id
uid=1000(zaeem) gid=1000(zaeem) groups=1000(zaeem),27(sudo)

$ ps -o pid,ruser,euser,comm -p $$
    PID RUSER    EUSER    COMMAND
   3901 zaeem    zaeem    bash

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 23  2026 /usr/bin/passwd

$ find /usr/bin /usr/sbin -perm -4000 -type f 2>/dev/null
/usr/bin/passwd
/usr/bin/chsh
/usr/bin/su
/usr/bin/sudo
/usr/bin/mount
/usr/bin/umount
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/gpasswd

What to read out of it.

For your shell, RUSER and EUSER are both zaeem. No badge in play. Normal.

Now look closely at -rwsr-xr-x on passwd. In the owner's permissions there is an s where you would normally see an x. That single letter is the setuid flag. It means: when anyone runs this, run it as the file's owner — root.

That is why you, an ordinary user, can change your own password even though /etc/shadow is readable only by root. You are not root. passwd is, for a moment.

The find command lists every program on the machine that can do this. There are nine here. Each one is a program that any user can start and that will run with root's powers.

Now imagine this at 500 hosts. That find command is a standard security audit. The expected list is short and well known. A setuid binary that is not on the expected list — especially in /tmp, a home directory, or an application folder — is one of the most reliable signs of a compromised machine, because it is how an attacker keeps root access after the way they got in has been closed.

It is also why containers are run with --security-opt no-new-privileges (or allowPrivilegeEscalation: false in Kubernetes), which blocks a process gaining privileges this way, and why many teams strip setuid bits from images at build time.

🎯 Interview questions — What a process is

Q. What is a process, and what does the kernel store about it?

A process is a running instance of a program. The program is a passive file on disk; the process is that program actually executing, with its own memory, its own open files, and its own place in the instruction stream.

The kernel keeps a record for each one — usually called the process control block. It holds the PID, the parent's PID, the current state, the saved CPU registers, the memory map, the table of open file descriptors, the user and group IDs, CPU time used, and scheduling priority.

Go further than the textbook answer:

  • Say why the saved registers matter. They are what makes preemption possible. The kernel takes the CPU away hundreds of times a second, and the process carries on afterwards as if nothing happened, because its exact position was written down.
  • Point at where it is visible. "You can read most of that record from /proc/PID/status, and the memory map from /proc/PID/maps." That turns a definition into something you have actually looked at.

The detail that separates candidates: many people say a program and a process are "basically the same". Give the one-to-many relationship instead — one bash binary on disk, five people logged in, five processes. Then give the operational consequence: you can patch the file on disk while old processes keep running the old code from memory, which is exactly why patching without restarting leaves you vulnerable.

Q. What is PID 1, and why is it special?

PID 1 is the first user-space process the kernel starts at boot. On modern distributions that is systemd; historically it was SysV init. It is special for three reasons:

  1. It has no parent. Its PPID is 0, which is the kernel's way of saying "nobody started this". Everything else on the machine descends from it.
  2. It adopts orphans. When a process dies while its children are still running, those children are handed to PID 1. Section C5 shows this happening.
  3. It cleans up after the dead. As the adoptive parent it collects the exit status of those adopted children, which stops them piling up as zombies.

The container angle, which is where this question usually goes: in a container, PID 1 is your application, not an init system. Most applications were never written to adopt orphans or collect exit statuses, so in a busy container zombies can accumulate until the process table fills. That is the entire reason docker run --init exists, and why images often use tini or dumb-init.

The detail that separates candidates: PID 1 also cannot be killed by the default signal handling rules. Signals with a default action of "terminate" are simply not delivered to PID 1 unless it has explicitly installed a handler for them. This is why a container whose PID 1 ignores the shutdown signal takes the full grace period and then gets killed the hard way — the classic "why does my container take 30 seconds to stop" ticket.

Q. What is the difference between real and effective UID? Why does passwd work for a normal user?

The real UID is who the user actually is. The effective UID is the identity the kernel uses when it checks permissions. They are normally the same.

They differ when a program file has the setuid bit set. Running such a program sets your effective UID to the file's owner. /usr/bin/passwd is owned by root and is setuid, so while it runs it has root's permissions — which is how an ordinary user can update /etc/shadow, a file only root can write.

You can see the bit as an s in the owner's execute position: -rwsr-xr-x.

The details that separate candidates:

  • Name the risk precisely. A setuid-root program gains all of root's powers, not only the one it needs. A single bug in it is a full privilege escalation. This is why find / -perm -4000 is a standard audit, and why an unexpected setuid binary is treated as a compromise indicator rather than a curiosity.
  • Mention the modern replacement. Linux capabilities split root's power into around forty separate permissions, so a program that only needs to bind to a low port can be given just that one instead of all of root. ping moved from setuid to CAP_NET_RAW for exactly this reason.
  • Know that scripts are excluded. Linux ignores the setuid bit on shell scripts, because the gap between checking the file and running the interpreter could be exploited. Candidates who claim you can make a setuid shell script are showing they have not tried it.

🐣 Part B · How a new process is born

B1 · fork — one process becomes two

Here is a question worth stopping on. If every process is started by another process, how does a process start another one?

You might expect a system call like "run this program". Linux does not work that way. It splits the job into two steps, and this section is the first one.

The first step is fork. It does one thing:

fork makes a copy of the process that called it.

That is all. After fork there are two processes. They have the same program, the same variables, the same open files, and both are sitting at the same point in the code. They are near-identical twins.

Two things are different, and only two matter right now:

  • The copy gets a new PID.
  • The copy's parent is the original.

There is one more clever detail. fork is a single system call, but it returns twice — once in each process. The original gets back the child's PID. The copy gets back 0. That is how each one works out which it is.

Real-world analogy — a copy of you, mid-task

You are at your desk, halfway through filling in a form. Someone makes a perfect copy of you and your desk, right now. Same half-finished form, same pen in hand, same coffee.

The copy is not a fresh new employee starting the day. It carries on from the exact word you were on. That is the part people find odd about fork and it is the most important part: the child does not start at the beginning of the program, it starts at the line after fork.

The only way you can tell each other apart is a note you are each handed. Yours says "you are the original, the copy is number 4127." The copy's says "you are the copy." That note is the return value.

Where the analogy stops working. Copying a person and a desk would take hours. fork is fast even for a huge process, and Section B2 explains the trick that makes that possible.

🧪 Exercise B1.1 — Watch one shell become two
bash
# $$ is your shell's PID. $BASHPID is the PID of whatever is running RIGHT NOW.
echo "My shell PID is: $$"

# Brackets make bash fork. Inside them you are in a NEW process.
( echo "Inside the brackets, I am PID: $BASHPID" )
( echo "Different brackets, different PID: $BASHPID" )

# Back outside, we are the original again
echo "Outside again, PID: $BASHPID"
Expected result — click to reveal
plain text
$ echo "My shell PID is: $$"
My shell PID is: 3901

$ ( echo "Inside the brackets, I am PID: $BASHPID" )
Inside the brackets, I am PID: 4127

$ ( echo "Different brackets, different PID: $BASHPID" )
Different brackets, different PID: 4128

$ echo "Outside again, PID: $BASHPID"
Outside again, PID: 3901

What to read out of it.

Every set of brackets produced a new PID. Each one was a real process, created by fork, that ran one echo and then exited.

Notice the numbers went 4127, then 4128. Each fork got the next number in line.

And notice the last line: 3901 again. The original shell never went anywhere. It made copies, the copies did their work and died, and the original carried on. That is the pattern behind almost everything a shell does.

Why $$ and $BASHPID are different. $$ deliberately keeps reporting the original shell's PID even inside a subshell, because scripts often need a stable identifier. $BASHPID always tells you the truth about the process you are in right now. Scripts that write a PID file using $$ from inside a subshell write the wrong number, and this is a real and common bug.
🧪 Exercise B1.2 — See the actual system call
bash
# Trace the fork. On Linux, fork is implemented by a syscall called clone.
strace -f -e trace=clone,clone3,execve -o /tmp/fork.txt \
  bash -c 'ls /tmp > /dev/null; echo done'

grep -E 'clone|execve|exited' /tmp/fork.txt | head -8
Expected result — click to reveal
plain text
$ grep -E 'clone|execve|exited' /tmp/fork.txt | head -8
4310  execve("/usr/bin/bash", ["bash", "-c", "ls /tmp > /dev/null; echo done"], 0x7ffd...) = 0
4310  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD) = 4311
4311  execve("/usr/bin/ls", ["ls", "/tmp"], 0x5591...) = 0
4311  +++ exited with 0 +++
4310  +++ exited with 0 +++

What to read out of it. Read the PID at the start of each line — that is which process made the call.

  1. 4310 runs execve to become bash. That line is strace itself: it forks, the child marks itself traceable, then execs the command you asked for. It appears with or without -f.
  2. 4310 calls clone and gets back 4311. One process just became two. The number it got back is the child's PID, exactly as described above.
  3. 4311 — the child — calls execve and becomes ls. That is Section B3.
  4. 4311 exits, then 4310 exits.
Why does it say clone and not fork? Because Linux only really has one process-creation call, clone, and it takes flags saying how much to share between parent and child. Classic fork is clone with "share nothing". Creating a thread is clone with "share almost everything". So when you see clone in a trace, you are looking at fork — the manual page is still called fork(2), and interviewers still say "fork".

Notice we needed -f (follow children). Without it you would still see the clone, but none of 4311's lines — the child's execve and exit would simply be missing. Forgetting -f is one of the most common reasons people conclude strace is not working.

B2 · Copy-on-write — why copying a process is not slow

Section B1 should have bothered you.

If fork copies the whole process, then forking a database using 8 GB of memory means copying 8 GB. And your shell forks for nearly every command you type. That would be unusably slow.

It is not slow, because the copy is a lie.

When fork happens, the kernel does not copy the memory. It gives both processes pointers to the same memory, and marks all of it read-only. Both processes read happily from the same pages, sharing everything.

Then, the moment either one tries to write, the CPU blocks the write and calls the kernel. The kernel copies just that one small page, gives the writer its own private copy, and lets the write go ahead.

This is called copy-on-write: don't copy until somebody writes.

Real-world analogy — two students, one textbook

Two students are given "their own copy" of the same textbook. Buying two books is expensive, so the library does something smarter: it hands them one book and says "share it, but nobody writes in it."

For reading, this works perfectly. Both get everything they need, instantly, at no cost.

Then one student wants to scribble notes on page 40. Only now does the library photocopy page 40 — one page, not the book — and give it to her. She writes on her copy. The other student still sees the clean original.

If she never writes on page 200, page 200 is never copied. Most pages never get copied at all.

Where the analogy stops working, and it is worth knowing. The library can always find another photocopier. The kernel cannot always find free memory. If a forked process starts writing everywhere at once and there is no memory left to make the copies, the machine is in real trouble — and this is exactly the situation that produces the out-of-memory killer in Module 09.

🧪 Exercise B2.1 — Fork a large process 200 times and time it
bash
# Put about 50 MB into a variable so this shell is genuinely large
BIG=$(head -c 50000000 /dev/zero | tr '\0' 'x')
echo "Variable holds ${#BIG} bytes"

# How big is this shell now?
grep VmRSS /proc/$$/status

# Now fork it 200 times. Each ( ) is a full copy of this 50 MB process.
# If fork really copied memory, this would move about 10 GB.
time ( for i in $(seq 1 200); do ( : ); done )

unset BIG
Expected result — click to reveal
plain text
$ echo "Variable holds ${#BIG} bytes"
Variable holds 50000000 bytes

$ grep VmRSS /proc/$$/status
VmRSS:	   52268 kB

$ time ( for i in $(seq 1 200); do ( : ); done )

real	0m0.148s
user	0m0.041s
sys	0m0.096s

What to read out of it.

The shell is using about 52 MB of real memory. It was forked 200 times. If fork copied memory, that is roughly 10 GB of copying — on a VM that only has 2 GB of RAM in total.

It took 0.148 seconds, and the machine never came close to running out of memory.

That is copy-on-write, measured. The memory was never copied, because none of those 200 children ever wrote to it. They ran : (do nothing) and exited.

This is the answer to a very common interview question. "Isn't fork expensive?" The strong answer is: "No, because of copy-on-write. The pages are shared read-only and only copied when one side writes. The real cost of fork is copying the page tables, not the pages — so it scales with how much memory is mapped, not how much is used."

That last sentence is what separates a strong candidate. It also explains the real production failure: forking a process with a very large mapped address space (a big JVM, a large Redis) does get slow, and it gets slower as the map grows, even though the data itself is never copied.

Now imagine this at 500 hosts. Redis uses fork to write its snapshots — the child gets a frozen view of the data at no cost and writes it to disk. But if the parent is busy taking writes, every changed page must now be copied for real. A Redis holding 8 GB can briefly need close to 16 GB during a save. Teams who sized the host at 10 GB discover this during their first busy backup, and the process is killed. The fix is vm.overcommit_memory=1 plus honest sizing — and it is Module 09's material.

B3 · exec — becoming a different program

Official docs: execve(2) · fork(2) · proc_pid_status(5)

fork gave us a second copy of the same program. That is not much use on its own — you wanted to run ls, not a second shell.

The second step is exec. It also does one thing:

exec throws away the program this process is running and loads a different program in its place.

The process keeps its PID. It keeps its parent. It keeps its open files. But its memory is wiped and replaced with the new program, which starts from its beginning.

And here is the part people find surprising: exec does not return. If it works, there is nothing to return to — the code that called it no longer exists. The only time exec comes back is when it fails.

Real-world analogy — the shop that changes what it sells

A shop on the high street closes for a refit. Same building, same address, same phone number, same lease, same postbox. Everything is cleared out and a completely different business opens: it was a bakery, now it is a barber.

Customers with the old address still arrive at the right door. Post still comes to the same box. But nothing inside is the same, and the bakery is gone — there is no going back to it.

That is exec. Same PID (the address), same open files (the postbox), same parent. Completely different program inside. And the old program cannot resume, because it has been cleared out.

Where the analogy stops working, and it is the useful bit. A refit takes weeks and the shop is shut. exec is instant, and the "postbox" detail is not a nice extra — it is the whole design. Because open files survive exec, a shell can decide where a command's output goes before the command exists. That is how > works, and Section B4 shows it.

🧪 Exercise B3.1 — Prove the PID survives
bash
# Start a shell, note its PID, then have it BECOME ps.
# If exec works as described, ps will report that same PID.
bash -c 'echo "The shell is PID: $$"; exec ps -o pid,comm'
Expected result — click to reveal
plain text
$ bash -c 'echo "The shell is PID: $$"; exec ps -o pid,comm'
The shell is PID: 4455
    PID COMMAND
   3901 bash
   4455 ps

What to read out of it. This is the clearest demonstration in the module.

The shell announced it was PID 4455. Then ps reported PID 4455, running ps. (The 3901 bash line is just your interactive shell — bare ps lists everything on your terminal.)

No new process was created. The count of processes on the machine did not change. One process simply stopped being bash and started being ps, keeping its number.

This is why "exec creates a process" is wrong, and why interviewers like the question. fork creates processes. exec replaces the contents of one.

🧪 Exercise B3.2 — Watch exec fail (this one is meant to fail)
bash
# exec a program that does not exist.
# Predict first: does the echo on the next line run?
bash -c 'exec /no/such/program; echo "Did you see this line?"'
echo "Exit code was: $?"

# And the same thing for a real program, so you can compare
bash -c 'exec /bin/true; echo "Did you see this line?"'
echo "Exit code was: $?"
Expected result — click to reveal
plain text
$ bash -c 'exec /no/such/program; echo "Did you see this line?"'
bash: line 1: /no/such/program: No such file or directory
$ echo "Exit code was: $?"
Exit code was: 127

$ bash -c 'exec /bin/true; echo "Did you see this line?"'
$ echo "Exit code was: $?"
Exit code was: 0

What to read out of it. Look at what did not happen: "Did you see this line?" was never printed, in either case. That is the lesson, and both halves teach it.

  • When exec worked (/bin/true), the echo was destroyed along with the rest of the shell before it could run. Nothing to return to.
  • When exec failed, exec did return — but a non-interactive shell treats a failed exec as fatal and exits immediately with 127.

127 is worth memorising. It means "command not found". When a CI job or a container exits with 127, the program you asked for was not there — wrong path, missing package, wrong architecture, or a script whose #! line points at an interpreter that does not exist. It is almost never a bug in your code.

Now imagine this at 500 hosts. Exit code 127 is one of the most common container startup failures there is. The image built fine, the manifest is valid, and the container dies instantly. Nine times out of ten it is a missing binary or a shell script with a #!/bin/bash line inside an Alpine image, which only ships /bin/sh. Knowing 127 by sight saves an hour every time.

B4 · fork and exec together — how your shell runs a command

Official docs: fork(2) · execve(2) · wait(2)

Now put the two halves together. This is what happens every single time you type a command and press enter.

Diagram source
sequenceDiagram
    participant U as You
    participant S as Your shell - bash
    participant C as The child process
    U->>S: you type ls and press enter
    S->>C: fork - make a copy of myself
    Note over C: the copy is still bash<br>with a new PID
    C->>C: set up where my output goes
    C->>C: exec - throw bash away, become ls
    S->>S: wait - pause until the child is done
    C->>S: exit - here is my status number
    S->>U: print the prompt again

The obvious question: why two steps? Why not one call that says "run ls"?

Because of the gap in the middle. Between the copy and the replacement, the child is still the shell, and it can change things about itself before the new program starts. It can decide where output goes. It can change user. It can close files it does not want the new program to have.

That gap is not an accident. It is the whole point.

This is how > works, and it is much simpler than people expect.

When you type ls > out.txt, the shell does not pass the filename to ls. ls has no idea a file is involved. It just writes to output number 1, exactly as always.

What happened is that in the gap between fork and exec, the child opened out.txt and made it output number 1. Then it became ls. The new program inherited that arrangement and wrote to the file without ever knowing.

This is why redirection works with every command on the system, including ones written before your shell existed. Module 03 takes this apart properly.

🧪 Exercise B4.1 — Watch the full sequence, including the redirection
bash
strace -f -e trace=clone,clone3,execve,openat,dup2,wait4 -o /tmp/seq.txt \
  bash -c 'ls /etc/hostname > /tmp/out.txt; echo finished'

grep -E 'clone|execve|out.txt|dup2|wait4' /tmp/seq.txt | head -10
Expected result — click to reveal
plain text
$ grep -E 'clone|execve|out.txt|dup2|wait4' /tmp/seq.txt | head -10
4501  execve("/usr/bin/bash", ["bash", "-c", "ls /etc/hostname > /tmp/out.tx"...], ...) = 0
4501  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|...|SIGCHLD) = 4502
4501  wait4(-1,  <unfinished ...>
4502  openat(AT_FDCWD, "/tmp/out.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
4502  dup2(3, 1)                        = 1
4502  execve("/usr/bin/ls", ["ls", "/etc/hostname"], ...) = 0
4501  <... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 4502

What to read out of it — this is the whole module in eight lines. Follow the two PIDs.

  1. 4501 is the shell. It calls clone and gets 4502. Now there are two.
  2. 4501 immediately calls wait4 and stops there. The parent is now asleep, waiting. That is Section C3.
  3. 4502 — still bash at this moment — opens /tmp/out.txt and gets file number 3.
  4. 4502 calls dup2(3, 1). In plain terms: "make my output number 1 point at that file instead of the screen." This is the redirection, and it happens before ls exists.
  5. 4502 calls execve and becomes ls. ls writes to output 1 as it always does. The bytes go to the file.
  6. The parent's wait4 wakes up and collects the result: exited, status 0. (The child's own exit line is not shown because it does not match our grep pattern — add |exited to see it.)

Look at the order once more. The file was opened by bash, not by ls. That is why > works identically for every command on the system.

Interview-grade detail. If asked "why does Linux use fork and exec instead of a single spawn call?", the answer is the gap. Everything a shell does between them — redirection, pipes, changing user, setting resource limits, closing descriptors — happens in a normal process using normal calls, so no single "run a program" call has to grow options for all of it.

The honest counter-argument is worth adding: the split is wasteful when the child immediately execs, which is why posix_spawn and vfork exist, and why huge processes use posix_spawn to avoid duplicating page tables. Giving both sides of that shows judgement rather than recitation.

🎯 Interview questions — Creating processes

Q. What is the difference between fork and exec?

fork creates a process by copying the calling one. Afterwards there are two processes running the same program, with different PIDs, both continuing from the line after the fork.

exec replaces the program running inside an existing process. No new process is created. The PID, the parent, and the open file descriptors all survive; the memory is wiped and the new program starts from its beginning.

The one-line version: fork gives you another process, exec gives you another program.

Then explain how they are used together, because that is what the question is really asking. To run a command, a shell forks and then the child execs. The parent stays alive to wait for the result.

The details that separate candidates:

  • fork returns twice, exec returns never. fork returns the child's PID to the parent and 0 to the child — that is how each knows which it is. A successful exec cannot return, because the code that called it no longer exists. If exec returns, it failed.
  • Prove it rather than assert it. "bash -c 'echo \\$\\$; exec ps' prints the same PID twice — no process was created, one just changed program."
  • Name why the split exists: the gap between them is where the shell sets up redirection, pipes and permissions, using ordinary calls in an ordinary process.
Q. Isn't fork expensive? It copies the whole process.

No, because of copy-on-write. fork does not copy the memory. Both processes are pointed at the same physical pages, marked read-only. When either one writes, the CPU traps it, the kernel copies that single page, and the writer gets a private copy. Pages that are only ever read are never copied.

The details that separate candidates:

  • Say what actually does get copied: the page tables. The cost of fork scales with the size of the address space that has to be described, not with how much data is in it. This is why forking a process with an enormous mapped region is genuinely slow even though nothing is duplicated.
  • Give the real production failure. Redis forks to write a snapshot. The child gets a free frozen view — but every page the parent modifies during the save must now really be copied. A Redis holding 8 GB can transiently need close to 16 GB, which is how teams get their cache OOM-killed during the first busy backup after go-live.
  • Know the alternatives and why they exist. vfork and posix_spawn skip the address-space duplication for the common "fork then immediately exec" case. Large runtimes prefer posix_spawn for exactly this reason.
Q. What does exit code 127 mean? What about 126?

127 means "command not found". The shell tried to exec the program and the exec failed because there was nothing at that path. 126 means "found, but could not execute" — usually the execute permission bit is missing, or it is a directory, or the binary is for the wrong architecture.

Where they actually come from, which is the better answer: these are shell conventions, not kernel ones. The kernel returned ENOENT or EACCES from execve; the shell chose to exit with 127 or 126 to report it. So the number is the shell's translation of an errno, which ties straight back to how system calls report failure.

The detail that separates candidates: name the container case, because that is where you will actually meet it. A container exiting instantly with 127 is nearly always one of three things: the binary is genuinely missing from the image; the script's #! line names an interpreter the image does not have (#!/bin/bash inside Alpine, which only ships /bin/sh); or the binary is dynamically linked against a libc the image does not have (a glibc build dropped into an Alpine/musl image). All three look identical from the outside, and ldd inside the image separates them in one command.

Bonus: 128 + N means the process was killed by signal N. So 137 is 128 + 9 — killed by SIGKILL, which in a container almost always means the out-of-memory killer. 143 is 128 + 15, a normal shutdown. Recognising 137 on sight is worth a lot in a Kubernetes interview.


🪦 Part C · How a process ends

C1 · Asking a process to stop

Official docs: kill(1) · signal(7) · ps(1)

Before we can talk about processes ending, you need one small idea.

A signal is a short message the kernel delivers to a process. There are about 30 of them and they have a whole topic to themselves later. For now you need exactly two.

SignalWhat it meansCan the program refuse?
TERM (15)"Please stop." This is what kill sends when you do not say otherwise.Yes. The program can catch it, finish what it is doing, and shut down tidily. It can also ignore it completely.
KILL (9)"Stop now." The kernel removes the process itself.No. The program is never told. It cannot catch it, block it, or ignore it. It gets no chance to clean up.

The command is called kill, which is a poor name — it sends a signal, and most signals do not kill anything.

Real-world analogy — asking someone to leave the building

It is closing time and someone is still working at their desk.

TERM is the security guard knocking and saying "we're closing in five minutes." The person saves their work, closes their files, logs off properly and walks out. Everything is left in a good state. They could ignore the knock — and some people do.

KILL is two guards picking up the chair with the person still in it and carrying them out of the door mid-sentence. It always works. The laptop is still open, the document is unsaved, and the office door is left swinging.

This is why you always try TERM first. Not politeness — the unsaved document. A database that gets KILLed has not flushed its buffers to disk. A web server that gets KILLed has dropped every connection mid-request.

Where the analogy stops working. A guard can always carry someone out. There is one situation where even KILL does nothing at all, and you will meet it in Section D2.

🧪 Exercise C1.1 — Watch a process ignore you (this is meant to fail)
bash
# Start a process that catches TERM and refuses to die
bash -c 'trap "echo I am ignoring that" TERM; while :; do sleep 1; done' &
STUBBORN=$!
echo "Started PID $STUBBORN"
sleep 1

# Ask it politely. Predict what happens first.
kill $STUBBORN
sleep 1
ps -o pid,stat,comm -p $STUBBORN

# Now do not ask.
kill -9 $STUBBORN
sleep 1
ps -o pid,stat,comm -p $STUBBORN
Expected result — click to reveal
plain text
$ echo "Started PID $STUBBORN"
Started PID 4780

$ kill $STUBBORN
$ sleep 1
I am ignoring that
$ ps -o pid,stat,comm -p $STUBBORN
    PID STAT COMMAND
   4780 S    bash

$ kill -9 $STUBBORN
$ sleep 1
$ ps -o pid,stat,comm -p $STUBBORN
    PID STAT COMMAND
[1]+  Killed                  bash -c 'trap ...'

What to read out of it.

kill printed no error. It succeeded — the signal was delivered. The process simply chose to print a message and carry on. ps proves it is still there.

That is the entire point. kill succeeding does not mean the process stopped. It means the message was sent. Scripts that run kill and immediately assume the process is gone are wrong, and this is a very common bug in shutdown scripts.

kill -9 produced no message from the program at all, because the program was never told. The kernel simply removed it. Then ps finds nothing.

Why you should still not reach for kill -9 first. Every real service catches TERM on purpose, so it can finish in-flight requests, flush data to disk, deregister from a load balancer and close connections cleanly. kill -9 skips all of that.

The correct order is always: send TERM, wait a sensible number of seconds, and only then send KILL. That is exactly what systemctl stop does (TimeoutStopSec), and what Kubernetes does (terminationGracePeriodSeconds, default 30). Both are just "TERM, wait, KILL" with a timer.

Now imagine this at 500 hosts. "Why does my container take 30 seconds to stop?" is almost always this: TERM is being sent, the process is not handling it, and the platform is waiting out the full grace period before using KILL. The usual cause is that PID 1 in the container is a shell script that started the real program as a child — the shell gets the TERM and the actual application never hears about it.

C2 · Exit status — the one number a process leaves behind

When a process finishes, it leaves behind exactly one piece of information: a number between 0 and 255.

That is all. Not a message, not a log, not an error object. One number, called the exit status.

The convention is simple and universal:

  • 0 means success.
  • Anything else means failure.

Note that this is backwards from what you might expect. Zero is the good one. There is only one way to succeed, and many different ways to fail, so the failures get all the other numbers.

In the shell, $? holds the exit status of the last thing that ran.

NumberWhat it meansWhere you meet it
0SuccessEverywhere
1General failure. The most common catch-all.Everywhere
2Often "you used the command wrongly"Many GNU tools
126Found the program, could not run it (not executable, wrong architecture)Shells, CI, containers
127Command not foundShells, CI, containers
130You pressed Ctrl-C. That is 128 + 2.Interactive use
137Killed by signal 9. That is 128 + 9.Very often the out-of-memory killer
143Stopped by signal 15. That is 128 + 15.Normal shutdown
The 128 + N rule is worth learning properly. A process that is killed by a signal does not choose an exit status — it never got the chance. So the shell reports 128 + the signal number instead, as a way of telling you "this did not exit, it was killed, and here is what killed it."

So when you see 137, subtract 128 and you get 9. Signal 9 is KILL. Something forcibly removed this process. In a container, that "something" is almost always the memory limit.

This single piece of arithmetic turns a meaningless number into a diagnosis, and it comes up constantly in Kubernetes work.

Real-world analogy — the delivery driver's receipt

A courier finishes a job and hands one slip back to the office. Not a report, not a story — a slip with a code on it.

0 means delivered. Other codes mean different failures: nobody home, wrong address, refused, van broke down. The office does not need the driver's account of the day. It needs the code, because the code decides what happens next: retry, refund, or escalate.

This is exactly why exit statuses exist and why they are one number. make, CI pipelines, systemd, Kubernetes and && in your shell all make decisions from that number alone. cmd1 && cmd2 means "run cmd2 only if cmd1 returned 0."

Where the analogy stops working, and it is the interesting bit. A driver who is knocked unconscious cannot hand in any slip. Someone else has to write "did not return" on their behalf. That is 128 + N — the process was killed and never filed a status, so the shell reports what happened to it instead.

🧪 Exercise C2.1 — Collect a set of exit statuses
bash
true;  echo "true       -> $?"
false; echo "false      -> $?"

ls /definitely/not/here 2>/dev/null; echo "missing dir -> $?"
notarealcommand 2>/dev/null;         echo "bad command -> $?"

# A process that kills itself with signal 9
bash -c 'kill -9 $$'; echo "killed -9   -> $?"

# A process that stops on signal 15
bash -c 'kill -15 $$'; echo "killed -15  -> $?"

# Exit statuses only go up to 255. Watch what happens past that.
bash -c 'exit 300'; echo "exit 300    -> $?"
Expected result — click to reveal
plain text
true       -> 0
false      -> 1
missing dir -> 2
bad command -> 127
killed -9   -> 137
killed -15  -> 143
exit 300    -> 44

What to read out of it.

true and false are real programs whose entire job is to return 0 and 1. That is how central this convention is.

ls on a missing directory returns 2, not 1. Different tools use different numbers for different failures, so "non-zero" is the only thing you can rely on across all of them.

137 and 143 are the 128 + N rule in action. Subtract 128: signal 9 and signal 15.

And exit 300 gave you 44. The status is only 8 bits, so 300 wraps around (300 − 256 = 44). This is a genuine trap: a script that does exit $(some_count) can report success by accident when the count happens to be 256, 512, or 768.

Now imagine this at 500 hosts. kubectl describe pod showing Exit Code: 137 is one of the most common things you will read in a Kubernetes job. It means SIGKILL. Combined with Reason: OOMKilled it means the container went over its memory limit. Without that reason line, 137 usually means the grace period expired — the app ignored TERM and the platform used KILL. Two very different fixes, told apart by one field.

C3 · wait — the parent collects the result

Official docs: wait(2) · fork(2) · proc_pid_stat(5)

A process has finished and left one number behind. Where does that number go?

It cannot be thrown away — the parent usually needs it. Your shell needs ls's exit status to set $?. So the kernel holds on to it until somebody asks.

The parent asks by calling wait. wait does two things:

  1. It pauses the parent until a child finishes (if none has yet).
  2. It hands over the finished child's exit status — and only then does the kernel throw away the child's record completely.

That second part has a name. Collecting a finished child is called reaping.

Diagram source
flowchart TD
    A["Child is running"]
    B["Child calls exit<br>with a status number"]
    C["Child's memory is freed<br>BUT the kernel keeps a small record<br>holding the exit status"]
    D["Parent calls wait<br>and collects the status"]
    E["Record deleted.<br>The PID is now free to reuse."]
    A --> B
    B --> C
    C --> D
    D --> E

Look carefully at box C. There is a window where the process is finished but not gone. Its memory is released, it uses no CPU, but a small record remains because the exit status has not been collected yet. That window is normally microseconds long. When it is not, you have a zombie — Section C4.

Real-world analogy — the receipt on the dispatch counter

Carrying on from the courier: the driver comes back, drops the receipt on the dispatch counter, and goes home. The driver is gone — van returned, shift over, costing the company nothing.

But the receipt is still on the counter, and the job is not closed. Dispatch has to pick it up, read the code, and file it. Only then is the job finished and the paperwork cleared away.

Normally this happens within seconds and the counter stays empty. The receipt is tiny — a single slip of paper. It is not the van.

Where the analogy stops working, and it is the whole reason the next section exists. If dispatch never picks up the receipts, the driver is still gone and no fuel is being burned — but the counter fills up with paper. Eventually there is no room to put a new receipt down, and no new jobs can be dispatched at all. That is a zombie problem, and it is why zombies matter even though they use no memory and no CPU.

🧪 Exercise C3.1 — Watch a parent wait, and collect the status
bash
# Start something in the background, then explicitly wait for it
sleep 3 &
CHILD=$!
echo "Started child $CHILD, now waiting..."
wait $CHILD
echo "Child finished with status: $?"

# Now one that fails, so you can see the status travel back
bash -c 'sleep 1; exit 42' &
wait $!
echo "That one returned: $?"
Expected result — click to reveal
plain text
$ sleep 3 &
[1] 4901
$ echo "Started child $CHILD, now waiting..."
Started child 4901, now waiting...
$ wait $CHILD
$ echo "Child finished with status: $?"
Child finished with status: 0

$ bash -c 'sleep 1; exit 42' &
[1] 4912
$ wait $!
$ echo "That one returned: $?"
That one returned: 42

What to read out of it.

The 42 is the point. That number was chosen inside a completely separate process, which then ceased to exist. The kernel held it, and wait delivered it to the parent. That is the only channel through which a finished process can tell its parent anything.

Notice also that wait blocked. Your shell sat there for three seconds doing nothing. That is normal — the parent was asleep, using no CPU, waiting for an event. In Module 01's terms, it was in state S.

This is also what your shell does every time you run a command in the foreground: fork, exec, wait. It is why you do not get a prompt back until the command finishes, and why adding & (which skips the wait) gives you the prompt immediately.

C4 · Zombies — the result nobody collected

A zombie is a process that has finished, but whose parent has not called wait to collect its exit status.

Everything about the process is already gone — its memory, its open files, its CPU time. What remains is the small record holding the exit status, waiting to be collected.

ps shows zombies with the state letter Z and the word <defunct>.

The thing everyone gets wrong: you cannot kill a zombie.

kill -9 on a zombie does nothing at all. Not because it is powerful, but because there is nothing left to kill. The process is already dead. You are firing at a filing card.

The only way to clear a zombie is to make its parent collect it. Either the parent starts calling wait, or the parent itself exits — at which point the zombie is handed to PID 1, which always collects.

So the practical answer to "how do I get rid of these zombies?" is nearly always restart the parent process. And the real fix is to repair the parent, because a program creating zombies has a bug.

Real-world analogy — receipts piling up on the counter

The drivers have all gone home. No vans are running, no fuel is being used, no wages are being paid. In every meaningful sense the work is finished.

But nobody in dispatch is filing the receipts, so the counter is now covered in slips of paper. It looks harmless — it is only paper.

Then one day the counter is full, and a returning driver has nowhere to put their slip. Now no job can be closed, and dispatch cannot send anyone out either, because every job needs a free space on that counter. The company has ground to a halt over paperwork, with a full fleet of working vans sitting outside.

That is exactly a zombie outbreak. Each zombie uses no memory and no CPU. But each one holds a PID, and PIDs are a finite pool. Fill it and the machine can no longer create any new process — you cannot even log in to fix it, because logging in requires forking.

Where the analogy stops working. You can throw away paper. You cannot throw away a zombie directly — only the original dispatcher is allowed to file its own receipts. If you want the counter cleared, you replace the dispatcher.

🧪 Exercise C4.1 — Create a real zombie, then fail to kill it
bash
# A parent that starts three short-lived children and then goes to sleep
# WITHOUT ever calling wait. They die at 0.1s; the parent naps for 60s.
bash -c 'for i in 1 2 3; do sleep 0.1 & done; sleep 60' &
PARENT=$!
echo "Parent is $PARENT"
sleep 3

# Find the zombies belonging to THIS parent
ps -eo pid,ppid,stat,comm | awk -v p="$PARENT" 'NR==1 || ($3 ~ /^Z/ && $2==p)'

# Grab one and try to kill it. Predict the result first.
ZOMBIE=$(ps -eo pid,ppid,stat --no-headers | awk -v p="$PARENT" '$3 ~ /^Z/ && $2==p {print $1; exit}')
echo "Zombie PID is $ZOMBIE"
kill -9 "$ZOMBIE"
sleep 1
ps -o pid,ppid,stat,comm -p "$ZOMBIE"

# Now kill the PARENT instead, and look again
kill "$PARENT"
sleep 1
ps -o pid,stat,comm -p "$ZOMBIE" >/dev/null 2>&1 || echo "Zombie is gone."
Expected result — click to reveal
plain text
$ ps -eo pid,ppid,stat,comm | awk -v p="$PARENT" 'NR==1 || ($3 ~ /^Z/ && $2==p)'
    PID    PPID STAT COMMAND
   5033    5032 Z    sleep
   5034    5032 Z    sleep
   5035    5032 Z    sleep

$ echo "Zombie PID is $ZOMBIE"
Zombie PID is 5033

$ kill -9 "$ZOMBIE"
$ sleep 1
$ ps -o pid,ppid,stat,comm -p "$ZOMBIE"
    PID    PPID STAT COMMAND
   5033    5032 Z    sleep

$ kill "$PARENT"
$ sleep 1
$ ps -o pid,stat,comm -p "$ZOMBIE" >/dev/null 2>&1 || echo "Zombie is gone."
Zombie is gone.

What to read out of it — the middle block is the lesson.

kill -9 on the zombie ran without an error, and the zombie is still listed, unchanged. Compare that with Exercise C1.1, where kill -9 removed a running process instantly. The difference is that here there is nothing to remove. kill -9 delivers a signal to a running process; this process finished several seconds ago.

Killing the parent cleared all three immediately. When the parent died, the zombies were handed to PID 1, which collected them at once. That is the fix, every time.

Why three children and not one? Because with a single background child, bash would very likely reap it for you while waiting, and no zombie would appear at all. Starting several makes the effect reliable — and it is closer to the real production shape, which is a parent forking many workers and reaping none of them.

Notice COMMAND still says sleep. The kernel keeps the name so ps can tell you what died. On a real system this is your best clue about which program the parent keeps forgetting to reap.

Now imagine this at 500 hosts. A handful of zombies is normal and harmless — reaping is not instantaneous. What matters is the trend. A count that climbs steadily and never falls means a parent with a bug, and the clock is now running against pid_max.

Diagnose it in two commands: ps -eo stat | grep -c Z for the count, then ps -eo ppid,stat | awk '$2 ~ /^Z/ {print $1}' | sort | uniq -c | sort -rn to find which parent is responsible. That second command names the guilty process directly, and it is almost always the same one.

C5 · Orphans — when the parent dies first

Official docs: credentials(7) · wait(2) · ps(1)

Section C4 was a child dying before the parent dealt with it. Now the other way round: the parent dies while the child is still running.

The child is called an orphan, and it is fine. Nothing bad happens to it. It keeps running exactly as before.

But it now has a problem: when it eventually finishes, there is nobody to collect its exit status. It would become a zombie forever.

So the kernel re-parents it. The orphan is handed to PID 1, which becomes its new parent. PID 1's job includes collecting the exit status of everything it has adopted, so the orphan will be reaped properly when its time comes.

Real-world analogy — the dispatcher who walks out

A driver is halfway through a delivery when their dispatcher quits and leaves the building.

Nothing happens to the driver. They are on the road, the parcel is in the van, and they carry on and complete the job. Losing your manager does not stop your work.

The problem is at the end of the shift: there is nobody at that desk to take the receipt. So head office automatically takes over every driver whose dispatcher has left. Head office always accepts receipts, so nothing piles up.

Where the analogy stops working, and it is the whole container problem. In a normal building, head office is a department whose actual job is handling paperwork. In a container, "head office" is your application — it is PID 1. Most applications were never written to accept paperwork from drivers they never hired. They ignore the receipts, and the counter fills up.

🧪 Exercise C5.1 — Make an orphan and watch it get adopted
bash
# Start a parent that lives for a few seconds and records its child's PID
bash -c 'sleep 60 >/dev/null 2>&1 & echo $! > /tmp/childpid; sleep 3' &
PARENT=$!
sleep 1
CHILD=$(cat /tmp/childpid)
echo "Parent is $PARENT, child is $CHILD"

# While the parent is still alive
ps -o pid,ppid,stat,comm -p "$CHILD"

# Wait for the parent to exit, then look again
sleep 4
ps -o pid,ppid,stat,comm -p "$CHILD"

# Who is that new parent?
NEWPARENT=$(ps -o ppid= -p "$CHILD" | tr -d ' ')
ps -o pid,comm -p "$NEWPARENT"

kill "$CHILD"
Expected result — click to reveal
plain text
$ echo "Parent is $PARENT, child is $CHILD"
Parent is 5209, child is 5210

$ ps -o pid,ppid,stat,comm -p "$CHILD"
    PID    PPID STAT COMMAND
   5210    5209 S    sleep

$ sleep 4
$ ps -o pid,ppid,stat,comm -p "$CHILD"
    PID    PPID STAT COMMAND
   5210       1 S    sleep

$ ps -o pid,comm -p "$NEWPARENT"
    PID COMMAND
      1 systemd

What to read out of it.

The PPID changed by itself, from 5209 to 1. Nothing you ran did that. The kernel re-parented the process the moment its original parent exited.

The STAT stayed S throughout. The orphan was never disturbed — it kept sleeping through the whole thing. Losing a parent does not harm a process.

Your new parent may not always be PID 1, and that is worth knowing.

Linux lets a process volunteer as a subreaper, meaning "give me the orphans from my part of the tree instead of sending them to PID 1." systemd uses this for services and for user sessions.

But a subreaper only catches orphans that are its own descendants. An SSH login descends from PID 1 by way of sshd, so an orphan created there goes to PID 1, which is what you see here. Start the same thing under systemd-run --user, or from a desktop session, and it is caught by systemd --user instead.

So the accurate rule is: an orphan goes to the nearest subreaper among its ancestors, and PID 1 if there is none. Candidates who say only "orphans go to init" are giving the pre-2013 answer; candidates who say "it always goes to systemd --user" have not checked the ancestry.

Now imagine this at 500 hosts. This mechanism is why nohup and disown work: your command survives your logout because it is simply re-parented and carries on.

It is also why "I killed the parent, why is the child still running?" is such a common confusion. Killing a parent does not kill its children. If you want the whole group gone you have to target the group — which is exactly why systemd tracks each service's processes in a cgroup and stops all of them together, rather than trusting a parent to tidy up.

🎯 Interview questions — Ending processes

Q. What is a zombie process, and what is an orphan?

A zombie is a process that has finished but whose exit status has not yet been collected by its parent. Its memory and resources are already released; all that remains is a small kernel record holding the exit status, plus the PID. ps shows it as Z / <defunct>.

An orphan is the opposite situation: a process still running whose parent has died. The kernel re-parents it, so it keeps running normally and will be reaped correctly when it finishes.

The clean contrast to give: a zombie is dead but not cleaned up; an orphan is alive but re-homed. An orphan is harmless. A zombie is harmless individually and dangerous in bulk.

The details that separate candidates:

  • Say why zombies matter, precisely. They use no memory and no CPU — but each holds a PID, and pid_max is finite. Enough of them and the machine cannot fork at all, which means you cannot even log in to fix it. Naming the resource that actually runs out is the answer interviewers are listening for.
  • You cannot kill a zombie, and explain why: there is nothing running to receive a signal. kill -9 is a no-op. The fix is to make the parent reap, or to kill the parent so PID 1 adopts and reaps it.
  • Give the modern re-parenting answer. Orphans go to the nearest subreaper, not necessarily PID 1. systemd registers user sessions and services as subreapers so each unit cleans up its own descendants. "Orphans go to init" is the older, less accurate answer.
Q. You find thousands of zombie processes on a production host. What do you do?

Work in this order and say it out loud:

  1. Confirm the scale and the trend. ps -eo stat | grep -c Z. A few is normal. What matters is whether it is climbing.
  2. Check how close you are to the ceiling. Compare against cat /proc/sys/kernel/pid_max. This tells you whether you have hours or minutes before the machine cannot fork.
  3. Find the guilty parent — this is the key step: ps -eo ppid,stat | awk '$2 ~ /^Z/ {print $1}' | sort | uniq -c | sort -rn. It will be one process, and now you know which service has the bug.
  4. Clear it now: restart that parent. Its zombies are re-parented to PID 1 or the nearest subreaper and reaped instantly. Do not waste time sending kill -9 to the zombies themselves.
  5. Fix it properly: the parent is forking children and never calling wait. Either it should reap them, or it should tell the kernel it does not want the statuses at all.

The details that separate candidates:

  • Never say kill -9 as the fix. Volunteering that it cannot work, and why, is worth more than the rest of the answer.
  • Name the container version, because that is where this happens now. If PID 1 in a container is your application, it inherits reaping duties it was never written for. docker run --init, or tini/dumb-init as PID 1, fixes it. Kubernetes has shareProcessNamespace considerations too.
  • The classic shape: a parent that forks workers and then blocks on something else forever — exactly the pattern in Exercise C4.1.
Q. What is the difference between kill and kill -9? Which should you use?

Plain kill sends SIGTERM (15): "please stop". The process is told, and can catch it to shut down cleanly — finish in-flight requests, flush buffers to disk, close connections, deregister from a load balancer. It can also ignore it.

kill -9 sends SIGKILL, which cannot be caught, blocked or ignored. The process is never told; the kernel removes it. No cleanup happens.

Always TERM first, then KILL only after a timeout. That is exactly what systemctl stop does with TimeoutStopSec, and what Kubernetes does with terminationGracePeriodSeconds.

The details that separate candidates:

  • kill succeeding tells you nothing about whether the process stopped. It only means the signal was delivered. Shutdown scripts that assume otherwise are a common bug.
  • Name what you lose with -9: unflushed database writes, dropped connections mid-request, stale lock files and PID files left behind, temporary files never cleaned up. "It's faster" is not a reason.
  • Know the case where -9 does nothing. A process in uninterruptible sleep (D) cannot be killed by any signal at all, and a zombie cannot either. Those are the two exceptions, and being able to name both is a strong signal of real experience.
  • The container angle: if a container ignores TERM, the usual cause is that PID 1 is a shell script that started the real application as a child. The shell receives the signal; the application never hears it. Use exec in the entrypoint so the application becomes PID 1 — which is exactly the exec behaviour from Section B3.
Q. Why does PID 1 matter in a container?

Because in a container, PID 1 is your application, not an init system — and PID 1 carries two responsibilities most applications were never written to handle.

  1. Adopting and reaping orphans. Any process whose parent dies inside the container is re-parented to PID 1. If your application ignores those exit statuses, they accumulate as zombies until the PID space is exhausted.
  2. Signal handling. The kernel does not apply default "terminate" actions to PID 1. A signal with a default action of terminate is simply not delivered unless PID 1 has explicitly installed a handler. So a PID 1 that ignores SIGTERM cannot be stopped politely at all — the platform waits out the grace period and then uses SIGKILL.

The fix is a small init process as PID 1: docker run --init, or tini / dumb-init in the image. They do nothing but forward signals and reap orphans.

The detail that separates candidates: name the shell-script entrypoint trap, because it is the one you will actually meet. An entrypoint like #!/bin/sh then myapp leaves the shell as PID 1 and the application as a child. TERM goes to the shell, which does not forward it, and the app never learns it should shut down. The one-word fix is exec myapp — the shell is replaced by the application, which becomes PID 1 itself and receives the signals directly. That single keyword is behind a very large number of "my container takes 30 seconds to stop" tickets.


🚦 Part D · Process states

D1 · The states you will actually see

You have already met some of these letters. Here is the full set, in one place.

At any moment, every process on the machine is in exactly one state. The kernel uses it to decide one thing: should this process be given CPU time right now?

LetterNameWhat it actually means
RRunning or runnableEither using a CPU right now, or ready and queuing for one. ps does not distinguish the two.
SInterruptible sleepWaiting for something — input, a timer, a network packet. Uses no CPU. Signals still reach it. Most processes, most of the time.
DUninterruptible sleepWaiting for the kernel to finish something that must not be interrupted, almost always disk or network storage. No signal reaches it, not even KILL.
TStoppedPaused. Ctrl-Z does this, and so does a debugger. It resumes exactly where it left off.
ZZombieFinished, exit status not yet collected. Section C4.
IIdle kernel threadAn idle kernel thread. Counted separately so it does not inflate the numbers. You will see a lot of these and can ignore them.
Diagram source
stateDiagram-v2
    [*] --> R : created by fork
    R --> S : waits for input or a timer
    S --> R : the thing it waited for happened
    R --> D : waits for disk or storage
    D --> R : the storage responded
    R --> T : paused by Ctrl-Z
    T --> R : resumed
    R --> Z : calls exit
    Z --> [*] : parent collects the status

The two arrows worth staring at are R --> S and R --> D. Both mean "waiting". The difference is whether the wait can be interrupted, and that single difference is the subject of the next section.

Real-world analogy — five people in an office
  • R — Sam is at his desk working. Or he is standing at the photocopier queue, ready to work the moment it is free. From outside, both look like "busy", which is exactly why ps uses one letter for both.
  • S — Priya is waiting for a phone call. She is doing nothing at all and costing nothing. If you tap her on the shoulder, she looks up. That is what "interruptible" means.
  • D — Dr Chen is halfway through an operation. You can tap her shoulder, shout, set off the fire alarm — she will not respond until she is finished, and that is correct, because stopping halfway would do real damage.
  • T — Marcus has been told to freeze and has not moved since. He will carry on from exactly where he was when told to continue.
  • Z — Aisha finished and went home an hour ago, but her timesheet is still sitting in the tray unsigned, so HR's system still lists her as on shift.

Where the analogy stops working, and it is worth flagging. Real people can always be interrupted eventually — a surgeon can be pulled out for something worse. A process in D genuinely cannot. There is no escalation, no override, no manager to call. You wait for the storage, or you reboot the machine.

🧪 Exercise D1.1 — Produce R and S side by side
bash
# A process that waits: it will be S
sleep 30 &
SLEEPER=$!

# A process that never stops working: it will be R
bash -c 'while :; do :; done' &
SPINNER=$!

sleep 1
ps -o pid,stat,pcpu,wchan:20,comm -p $SLEEPER,$SPINNER

# Now pause the spinner and look again
kill -STOP $SPINNER
sleep 1
ps -o pid,stat,pcpu,comm -p $SPINNER

kill -9 $SPINNER $SLEEPER 2>/dev/null
Expected result — click to reveal
plain text
$ ps -o pid,stat,pcpu,wchan:20,comm -p $SLEEPER,$SPINNER
    PID STAT %CPU WCHAN                COMMAND
   5412 S     0.0 hrtimer_nanosleep    sleep
   5414 R    99.3 -                    bash

$ kill -STOP $SPINNER
$ ps -o pid,stat,pcpu,comm -p $SPINNER
    PID STAT %CPU COMMAND
   5414 T    48.5 bash

What to read out of it.

sleep is S at 0.0% CPU. It is doing nothing and costing nothing.

The loop is R at 99.3% CPU. It always wants the CPU, so it is always either on one or queuing for one.

The WCHAN column is the useful one and most people never use it. It shows what the process is waiting for, by naming the kernel function it is parked in. hrtimer_nanosleep means "waiting for a timer to expire" — which is exactly what sleep does. The spinner shows - because it is not waiting for anything.

After kill -STOP, the state is T. The process still exists, still holds all its memory, and is simply not being scheduled. kill -CONT would resume it mid-instruction as if nothing had happened. This is what Ctrl-Z does.

Notice that %CPU did not drop to zero. That column is not a live rate — it is total CPU time divided by total lifetime, so it decays slowly as the clock keeps running while the CPU time stands still. The state letter is what tells you it stopped; the percentage lags behind. This catches people out when they watch %CPU expecting it to fall immediately.

WCHAN is the single most underused column in ps. When a process is stuck and you do not know why, ps -eo pid,stat,wchan:25,comm frequently names the reason directly. Waiting on a lock, on a network socket, on a disk read — each parks in a differently named kernel function. It turns "this process is hung" into "this process is waiting on this specific thing."

D2 · D state — the one you cannot kill

In Section C1 you learned that SIGKILL cannot be caught, blocked or ignored. That is true. But there is a situation where it still does nothing, and this is it.

When a process asks for data from storage, the kernel starts a low-level operation involving real hardware. Halfway through, the kernel is holding locks, has buffers in flight, and has told a device to write into a specific piece of memory. If a signal yanked the process out at that moment, the kernel would be left in a broken state — a half-finished filesystem update, a device writing into memory that has been freed.

So for those specific waits, the kernel puts the process into uninterruptible sleep, state D. Signals are not delivered. Not TERM, not KILL. The process comes back when the storage responds, and only then.

This is why kill -9 sometimes appears to do nothing.

There are exactly two cases where kill -9 has no effect, and you now know both:

  • A zombie — already dead, nothing left to signal.
  • A process in D state — alive, but not accepting signals until its I/O completes.

If the storage never responds — a dead disk, a hung NFS server, a detached network volume — the process stays in D forever. You cannot kill it. You often cannot unmount the filesystem either. Frequently the only fix is to make the storage answer, or to reboot.

Being able to say this in an interview, calmly and with both cases, is one of the strongest signals of real production experience in this entire module.

🧪 Exercise D2.1 — Catch a process in D state
bash
# Force real disk writes that bypass the cache, so the process must
# genuinely wait for hardware. Then sample fast enough to catch it.
dd if=/dev/zero of=/tmp/ddtest bs=1M count=800 oflag=direct 2>/dev/null &
DDPID=$!

for i in $(seq 1 40); do
  ps -o pid,stat,wchan:22,comm -p $DDPID --no-headers 2>/dev/null
  sleep 0.1
done | sort | uniq -c | sort -rn | head

wait $DDPID 2>/dev/null
rm -f /tmp/ddtest
Expected result — click to reveal
plain text
23       5601 D    submit_bio_wait        dd
14       5601 R    -                      dd
 3       5601 S    -                      dd

What to read out of it.

Out of 40 samples, the process was in D state 23 times — most of the time. It was waiting for the disk to physically accept the data.

WCHAN says submit_bio_wait. That is the kernel function that submits a block I/O request and waits for it to complete. The column has told you exactly what it is waiting for: the block layer.

During those 23 samples, that process was unkillable. A kill -9 sent then would have been recorded and only acted on once the write finished.

If you saw no D at all, nothing is wrong. On a fast SSD or a well-cached VM the waits can be too short to catch at 100 ms sampling. Try count=2000, or add conv=fsync. The point of the exercise is the pattern, not the exact count.
Now imagine this at 500 hosts. The classic incident is an NFS server going away. Every process that touches that mount enters D and stays there. ls hangs. df hangs. Your monitoring agent hangs. kill -9 does nothing on any of them, and the host looks alive but is effectively frozen.

The prevention is a mount option, decided long before the incident: mounting NFS with soft and a timeo lets the I/O fail with an error instead of waiting forever, which turns an unkillable hang into an ordinary error your application can handle. hard mounts favour data integrity and accept the hang. That is a real trade-off, and knowing you have to choose is the point.

D3 · What load average actually counts

We need one more idea to finish Part D properly, because D state produces a symptom that confuses almost everybody.

The three numbers from uptime are the load average. Nearly everyone believes they measure CPU usage. They do not.

On Linux, load average counts the processes that are:

  • in state R — running or waiting for a CPU, plus
  • in state D — in uninterruptible sleep.

That second half is the surprise, and it is unique to Linux. A process waiting on a dead disk uses zero CPU and still adds 1.0 to your load average.

So the famous symptom follows directly: very high load, and an almost completely idle CPU. That combination is not a CPU problem. It is nearly always storage.

Real-world analogy — counting people in a bank

The manager wants to know how busy the branch is, so she counts everyone who is not free to leave.

That includes customers being served and customers in the queue — the R group, genuinely competing for tellers. But it also includes anyone stuck in the safety-deposit room because the door mechanism has jammed — the D group. They are not being served. They are not in any queue. They are just unable to leave.

Her number reads "18 people occupied" while three of her four tellers stand idle. Both facts are true. Hiring more tellers would not help even slightly, because the problem is a jammed door.

That is a load average of 18 with a CPU that is 95% idle. The answer is not more CPU. The answer is to go and look at the storage.

Where the analogy stops working. The manager's number is a live count. Load average is an average smoothed over time — the three figures are roughly the last 1, 5 and 15 minutes. So a brief spike barely shows, and a resolved problem keeps showing for a while afterwards.

Read the three together, and note the direction carefully because it is easy to get backwards: the 1-minute figure being the highest means it is getting worse. The 15-minute figure being highest means it is recovering.

🧪 Exercise D3.1 — Make load average rise without using any CPU
bash
# Baseline
uptime
grep -c . /proc/loadavg >/dev/null; cat /proc/loadavg

# Start heavy uncached disk writes - these will sit in D state
for i in 1 2 3 4; do
  dd if=/dev/zero of=/tmp/load$i bs=1M count=600 oflag=direct 2>/dev/null &
done

sleep 25
echo "--- during the writes ---"
uptime
echo "CPU idle percentage:"
vmstat 1 2 | tail -1 | awk '{print $15"% idle,  "$16"% iowait"}'
echo "Processes in D state right now:"
ps -eo stat | grep -c '^D'

wait
rm -f /tmp/load*
Expected result — click to reveal
plain text
$ uptime
 12:04:11 up 1:22,  1 user,  load average: 0.08, 0.12, 0.09
$ cat /proc/loadavg
0.08 0.12 0.09 1/243 5702

--- during the writes ---
 12:04:39 up 1:23,  1 user,  load average: 3.42, 1.16, 0.41
CPU idle percentage:
88% idle,  9% iowait
Processes in D state right now:
4

What to read out of it — this is the payoff of Part D.

Load average went from 0.08 to 1.38 in 25 seconds, and it is still climbing. On a 2-CPU machine that already reads like trouble.

And the CPU is 88% idle. Almost nothing is being computed.

The two facts are reconciled by the last line: 4 processes in D state. Each one is waiting on the disk, using no CPU, and each adds 1.0 to the load average.

So why is the figure 1.38 rather than 4.0? Because load average is smoothed, not instantaneous. The 1-minute figure moves towards the true value gradually, and 25 seconds is not long enough to get there. Leave the writes running for three minutes and it climbs towards 4.0. This lag is exactly why a short spike can be invisible in the numbers.

iowait at 9% is the supporting clue. That is CPU time spent idle specifically because something is waiting for I/O.

Look at the three load figures together: 1.38, 0.42, 0.20. The 1-minute figure is the highest, which means this started recently and is still climbing. The reverse order — 15-minute highest — would mean the problem is easing off.

The fourth field of /proc/loadavg is worth knowing too: 1/243 means 1 process currently runnable out of 243 total.

This is a very common interview question, and most candidates get it wrong. "Load average is high but CPU is idle — what is going on?"

The weak answer talks about CPU contention. The strong answer: "On Linux, load average counts processes in uninterruptible sleep as well as runnable ones. High load with idle CPU means processes are blocked on I/O, not competing for CPU. I would check ps -eo stat | grep -c '^D', look at iowait in vmstat, then iostat -x for the device, and dmesg for storage errors. Adding CPU would not help."

Adding "this is Linux-specific — most other Unixes count only runnable processes" is the detail that ends the question.

D4 · Reading ps and top properly

Official docs: ps(1) · pstree(1) · proc_pid_stat(5)

ps has an unusual number of options because it accepts three different styles of them at once, for historical reasons. You do not need most of it. You need one habit: stop using ps aux and ask for the columns you actually want.

bash
ps -eo pid,ppid,stat,pcpu,pmem,wchan:20,etime,comm

Several extra characters can appear after the main state letter. These are the ones worth knowing:

SuffixMeaning
sSession leader — the process at the top of a login session or job
+In the foreground process group — it currently owns the terminal
lMulti-threaded
&lt; / NHigher than normal priority / lower than normal priority
Real-world analogy — the ward whiteboard

A hospital ward has a whiteboard listing every patient: bed number, who admitted them, current condition, how long they have been in.

Nobody stands and reads all forty rows. Staff scan one column depending on what they came for. Who is deteriorating? Read the condition column. Who has been here longest? Read the duration column.

ps is that whiteboard, and -eo lets you choose which columns go on it. ps aux is the equivalent of printing every field for every patient and reading it top to bottom — it works, and it is a slow way to find anything.

Where the analogy stops working, and it matters for accuracy. A whiteboard is updated by people over hours. ps is a single instant, read from /proc at the moment you pressed enter. A process can be R when you look and S a millisecond later. This is why Exercise D2.1 sampled 40 times instead of once — for anything that changes quickly, one ps is a photograph, not the film.

🧪 Exercise D4.1 — Build a ps command worth keeping
bash
# Everything useful, sorted by CPU, top 10
ps -eo pid,ppid,stat,pcpu,pmem,wchan:18,etime,comm --sort=-pcpu | head -11

# Count how many processes are in each state - a great one-line health check
ps -eo stat --no-headers | cut -c1 | sort | uniq -c | sort -rn

# Anything not healthy: zombies or stuck in D
ps -eo pid,ppid,stat,wchan:18,comm | awk 'NR==1 || $3 ~ /^[ZD]/'

# The full family tree of one service
pstree -ps $(pgrep -n sshd)
Expected result — click to reveal
plain text
$ ps -eo pid,ppid,stat,pcpu,pmem,wchan:18,etime,comm --sort=-pcpu | head -11
    PID    PPID STAT %CPU %MEM WCHAN              ELAPSED COMMAND
   5414    3901 R    99.3  0.2 -                    00:42 bash
      1       0 Ss    0.1  1.1 ep_poll             01:24:07 systemd
    812       1 Ss    0.0  0.6 ep_poll             01:23:44 sshd
   3901    3898 Ss    0.0  0.2 do_wait                12:03 bash
   5455    3901 R+    0.0  0.1 -                    00:00 ps
   ...   (rows 6 to 10 omitted)

$ ps -eo stat --no-headers | cut -c1 | sort | uniq -c | sort -rn
    198 S
     31 I
      8 R
      2 D
      1 Z

$ ps -eo pid,ppid,stat,wchan:18,comm | awk 'NR==1 || $3 ~ /^[ZD]/'
    PID    PPID STAT WCHAN              COMMAND
   5033    5032 Z    -                  sleep
   5601    3901 D    submit_bio_wait    dd

What to read out of it.

In the first table, look at the suffixes. systemd and sshd are Ss — sleeping session leaders. Your shell is Ss too. The ps command itself is R+ — running, and in the foreground group, because it owns your terminal right now.

WCHAN tells the story of the sleepers for free. systemd and sshd are in ep_poll, waiting on epoll for a connection or an event. Your shell is in do_wait — waiting for a child, which is exactly Section C3. Three sleeping processes, three genuinely different reasons, visible in one column.

The second command is the one to memorise. One line, and you know the health of the machine: 198 sleeping (normal), 8 runnable, 2 in D, 1 zombie. Run it on a healthy host to learn its normal shape, and the abnormal shape becomes obvious.

The third is your triage command. Z and D are the two states that indicate something is wrong, and this prints only those.

Now imagine this at 500 hosts. ps -eo stat --no-headers | cut -c1 | sort | uniq -c run across a fleet gives you a one-line health signature per host. Hosts whose signature has drifted — climbing Z, any sustained D — are the ones to look at first. It costs almost nothing to collect and it catches both of this module's failure modes before they page anyone.

🎯 Interview questions — Process states

Q. Explain the difference between D state and Z state, and why kill -9 fails on each.

They are opposite problems that share a symptom.

D — uninterruptible sleep. The process is alive and waiting inside the kernel for an I/O operation that must not be interrupted. Signals are not delivered at all, so SIGKILL is recorded but not acted on until the I/O completes. If the storage never responds, the process stays there indefinitely.

Z — zombie. The process is already dead. Its memory and resources are gone; only the exit status record remains, waiting for the parent to call wait. There is nothing running to receive a signal, so SIGKILL is a no-op.

The one-line contrast: D is too busy to die, Z is already dead.

The fixes are completely different. For D: fix the storage, or reboot. For Z: make the parent reap, or kill the parent so PID 1 adopts and reaps it.

The details that separate candidates:

  • Explain why D exists rather than just naming it. The kernel is holding locks and has a device mid-transfer; interrupting there would corrupt filesystem state or let hardware write into freed memory. It is a correctness guarantee, not an oversight.
  • Use WCHAN. ps -eo pid,stat,wchan:25,comm names the kernel function the process is parked in, which usually identifies the subsystem immediately — submit_bio_wait for the block layer, an nfs_ prefix for NFS.
  • Mention TASK_KILLABLE. Some newer waits — notably parts of NFS — use a middle state that blocks ordinary signals but still allows SIGKILL, added specifically because unkillable NFS hangs were so painful. So "D is always unkillable" is the older answer; "most D waits are unkillable, and some paths are now killable" is the current one.
Q. Load average is 20 but the CPU is 95% idle. What is happening?

The machine is not short of CPU. It is blocked on I/O.

On Linux, load average counts processes in uninterruptible sleep (D) as well as those running or runnable. A process waiting on storage consumes no CPU and still adds to the number. So a high load with an idle CPU means processes are stuck waiting, not competing.

How to confirm it, in order:

  1. ps -eo stat | grep -c '^D' — how many are actually blocked.
  2. vmstat 1 — expect a high wa (iowait) and a low r column.
  3. ps -eo pid,stat,wchan:25,comm | awk '$2 ~ /^D/' — what they are waiting on.
  4. iostat -x 1 — is a device at 100% utilisation with high await?
  5. dmesg -T | tail — storage errors, controller resets, NFS timeouts.
  6. mount | grep nfs — a hung network filesystem is the classic cause.

The details that separate candidates:

  • Say it is Linux-specific. Most other Unixes count only runnable processes. This is why the same number means different things on different systems, and it is a detail few candidates know.
  • Normalise by CPU count. A load of 20 on a 32-core box is not the same as on a 2-core box. Always ask "load relative to nproc".
  • Read the three numbers as a trend, not as one figure — 1, 5 and 15 minutes. The 1-minute figure being highest means it is getting worse; the 15-minute figure being highest means it is recovering. Also note they are smoothed, so a problem that started 20 seconds ago will not yet show its true size.
  • State the conclusion plainly: adding CPU or scaling out would not help. Fix the storage.
Q. A process will not die. Walk me through what you check.

Work down the states, because each one has a different cause and a different fix:

  1. ps -o pid,stat,wchan:25,comm -p PID first. The state letter tells you which problem you have before you try anything.
  2. State Z — it is already dead. Signals are pointless. Find the parent with ps -o ppid= -p PID and restart it.
  3. State D — it is blocked in the kernel and no signal will reach it. Read WCHAN to identify the subsystem, check dmesg and iostat for storage trouble, and look for a hung NFS or detached volume. Often the only resolutions are restoring the storage or rebooting.
  4. State S or R and still alive after TERM — the process is catching SIGTERM and ignoring it, or its handler is stuck. kill -9 will work here. This is the only one of the three where -9 is the answer.
  5. It dies and comes straight back — it is not the process, it is the supervisor. systemd with Restart=always, a Kubernetes controller, or a wrapper script is recreating it. Kill the supervision, not the process.

The details that separate candidates:

  • Leading with the state letter rather than escalating straight to kill -9 is the whole point of the question. It shows you diagnose before acting.
  • Knowing that -9 cannot help in two of the five cases, and being able to say which, is what an experienced interviewer is listening for.
  • Volunteering case 5 shows you have actually operated systems. It is very common and it catches people who only think about processes in isolation.

🏁 Part E · Practice, capstone, reference and review

E1 · Production practice

SituationWhat you now runWhat it tells you
Quick health check of any hostps -eo stat --no-headers | cut -c1 | sort | uniq -c | sort -rnThe state signature. Learn the normal shape; drift is the signal
Load average high, CPU idleps -eo stat | grep -c '^D', then vmstat 1 and iostat -x 1Blocked on I/O, not short of CPU. Adding CPU will not help
Zombie count climbingps -eo ppid,stat \| awk '$2 ~ /^Z/ {print $1}' \| sort \| uniq -c \| sort -rnNames the parent with the bug. Restart it; kill -9 on the zombies does nothing
Process will not dieps -o pid,stat,wchan:25,comm -p PIDZ = already dead · D = blocked in kernel · S/R = ignoring TERM
Process stuck, no idea whyps -eo pid,stat,wchan:25,commWCHAN names the kernel function it is parked in — usually the subsystem at fault
Unknown process found runningpstree -ps PID and ls -l /proc/PID/exeWho started it, and which binary it really is. A miner's parent chain runs through the app that was exploited
Container exits instantly, code 127Check the entrypoint path, then ldd the binary inside the imageMissing binary, missing interpreter on the #! line, or wrong libc
Container exits with 137kubectl describe pod, look for OOMKilled128 + 9 = SIGKILL. With OOMKilled it is the memory limit; without it, the grace period expired
Container takes 30s to stopRead the entrypoint. Is PID 1 a shell script?The shell gets TERM and never forwards it. Fix is exec in the entrypoint
Zombies inside a containerdocker run --init, or tini / dumb-init as PID 1Your app is PID 1 and was never written to reap adopted orphans
Killed the parent, child still runningps -o pid,ppid,comm -p CHILDPPID changed — it was re-parented, not killed. Target the cgroup, not the parent
Security audit of a hostfind / -perm -4000 -type f 2>/dev/nullEvery setuid binary. Anything off the known list is a compromise indicator

E2 · Capstone exercise

🧪 CAPSTONE — Build and diagnose a broken service, using only this module

Nothing new. Create each failure yourself, then diagnose it as if you had walked in cold.

bash
# ---------- FAULT 1: a parent that leaks zombies ----------
bash -c 'for i in $(seq 1 20); do sleep 0.1 & done; sleep 120' &
BAD=$!
sleep 4

# Diagnose without looking above. Answer in writing:
#   a) How many zombies, and is it growing?
#   b) Which PID is the parent responsible?
#   c) Why will kill -9 on the zombies not help?
#   d) What is the correct fix now, and the correct fix in the code?
ps -eo stat --no-headers | cut -c1 | sort | uniq -c | sort -rn
ps -eo ppid,stat | awk '$2 ~ /^Z/ {print $1}' | sort | uniq -c | sort -rn

kill $BAD; sleep 1

# ---------- FAULT 2: a process that ignores shutdown ----------
bash -c 'trap "" TERM; while :; do sleep 1; done' &
IGN=$!
sleep 1
kill $IGN; sleep 2
ps -o pid,stat,comm -p $IGN
#   e) kill reported no error. Why is the process still there?
#   f) What does a real platform do next, and after how long?
kill -9 $IGN

# ---------- FAULT 3: load that is not CPU ----------
for i in 1 2 3; do dd if=/dev/zero of=/tmp/cap$i bs=1M count=500 oflag=direct 2>/dev/null & done
sleep 15
uptime; vmstat 1 2 | tail -1; ps -eo pid,stat,wchan:20,comm | awk '$2 ~ /^D/'
#   g) Load is up. Is this a CPU problem? Prove your answer with two numbers.
#   h) Which column told you what they are waiting for?
wait; rm -f /tmp/cap*
What a good answer looks like — click to reveal

You are marked on whether your conclusion follows from your evidence, not on exact numbers.

a–d, the zombies. Around 20 in state Z. The second command prints one PID with a count next to it — that is the parent, and it is the answer. kill -9 cannot help because those processes are already dead; there is nothing to receive a signal. The fix now is to restart that parent, so its zombies are re-parented to PID 1 (or the nearest subreaper) and reaped at once. The fix in the code is for the parent to call wait for its children, or to tell the kernel it does not want their exit statuses. A complete answer also mentions the ceiling: zombies hold PIDs, and pid_max is finite.

e–f, the stubborn process. kill succeeded — the signal was delivered. The process installed a handler that discards TERM, so it simply ignored it. kill returning 0 means "sent", never "stopped", and code that assumes otherwise is a common shutdown bug. A real platform sends TERM, waits a fixed period, then sends KILL: systemd via TimeoutStopSec, Kubernetes via terminationGracePeriodSeconds (30s by default). This is exactly why some containers always take the full 30 seconds to stop.

g–h, the load. No, it is not a CPU problem, and two numbers prove it: load average is well above 1 while CPU idle is high (with wa elevated). Load average on Linux counts D-state processes as well as runnable ones, so blocked-on-storage work inflates it while using no CPU. The WCHAN column names what they are waiting for — a block-layer function such as submit_bio_wait. Scaling CPU or adding replicas would change nothing.

Why this is the capstone. All three faults present as "the service is unhealthy" and all three have a different cause, a different diagnostic, and a different fix. The skill being tested is not knowing three commands — it is checking the state before acting, which is the habit that separates people who fix incidents from people who restart things until the symptom goes away.

E3 · Official documentation reference

TopicOfficial pageOffline equivalent
Creating processesfork(2)man 2 fork
Replacing the programexecve(2)man 2 execve
Reaping, zombies, exit statuswait(2)man 2 wait
PIDs, PPIDs, UIDscredentials(7)man 7 credentials
Process states and columnsps(1)man 1 ps
The state field, rawproc_pid_stat(5)man 5 proc_pid_stat
Everything in /proc/PID/statusproc_pid_status(5)man 5 proc_pid_status
Signals overviewsignal(7)man 7 signal
Sending signalskill(1)man 1 kill
The process treepstree(1)man 1 pstree
/proc overviewproc(5)man 5 proc
The standardPOSIX.1-2024 Base Specifications Issue 8man 7 standards
Reading tip carried over from Module 01. For every page above, read DESCRIPTIONRETURN VALUEERRORSNOTES. For this module the NOTES section of fork(2) and wait(2) is where the genuinely surprising behaviour lives, and almost nobody reads that far.

Start with man 7 credentials and man 7 signal — the section 7 overviews explain the model, which makes the section 2 pages much easier afterwards.

E4 · Self-assessment

Answer out loud, without opening anything above.

  1. In one sentence each, what does fork do and what does exec do? Which one creates a process?
  2. fork copies a process. Why is it not slow for a process using 8 GB of memory? What does the cost scale with?
  3. A shell runs ls > out.txt. Who opens out.txt — the shell or ls? Explain why that is the only way it could work.
  4. What is a zombie? Why does kill -9 do nothing to one, and what actually clears it?
  5. What is an orphan, and what happens to it? Where exactly does it go?
  6. You see exit code 137. What happened? What about 127?
  7. Name the two situations where kill -9 will not remove a process, and explain why in each case.
  8. Load average is 15 and the CPU is 90% idle. What is your diagnosis, and which two commands would you run to confirm it?
  9. Why does PID 1 in a container matter? Name the two responsibilities most applications do not implement.
  10. A container takes the full 30 seconds to stop, every time. What is the most likely cause, and what is the one-word fix?

E5 · Sources

Interview questions were taken from published 2026 question sets and then extended with operational detail beyond the published answers.

Technical content is sourced from the official documentation listed in E3.

Next: Module 03 — File Descriptors, Inodes & Filesystems. Section B4 showed the shell opening a file and making it output number 1 without ls ever knowing. Module 03 explains what those numbers actually are.
Spotted a mistake or want something added? Send me a note.