<aside> 🛑
Module 13 — Signals and Traps. What really happens on Ctrl-C, why kill -9 is a last resort, and how trap makes scripts clean up after themselves even when they die mid-run.
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
</aside>
<aside> 📋
Before you start. You need Modules 1–12, and Module 12 above all: signals are messages between processes, so the parent/child/PID model is the grammar here. Old IOUs come due: the 128+N exit family (Module 3), SIGPIPE ending abandoned writers (Modules 4 and 11), what Ctrl-C sends (Module 6), what nohup immunizes against (Module 12). Tools: nothing new.
</aside>
<aside> 📖
Official docs: signal(7) — man7.org · kill(1) — man7.org
</aside>
Processes cannot read each other's memory (Module 12's separate notebooks) — but they can send signals: tiny numbered notifications delivered by the kernel. A process receiving one either performs the signal's default action (usually: terminate), or — if it installed a handler — runs its own response instead. kill PID is the sending tool, and its name oversells it: kill sends a message; by default the message is SIGTERM, a polite request. The daily cast:
| Signal | Number | Meaning / source | Catchable? |
|---|---|---|---|
SIGINT |
2 | "Interrupt" — what Ctrl-C sends to the foreground job | yes |
SIGTERM |
15 | "Please terminate" — kill's default; the polite shutdown request | yes |
SIGKILL |
9 | Immediate destruction by the kernel — the process never sees it | no — cannot be caught, blocked, or ignored |
SIGHUP |
1 | "Hang-up" — your terminal died; what nohup immunizes against (Module 12) | yes (daemons traditionally reload config on it) |
SIGPIPE |
13 | "You wrote to a pipe nobody reads" — Module 4's abandoned-writer ending | yes |
And the Module 3 debt, paid: a process killed by signal N exits with status 128+N. 130 = Ctrl-C'd (128+2). 143 = SIGTERMed (128+15). 137 = SIGKILLed (128+9) — the number every container operator learns to dread, because it is also what the out-of-memory killer leaves behind. Exit codes above 128 are death certificates with the cause filled in.
<aside> 🧩
Real-world analogy — ways to end a meeting. SIGINT is knocking on the meeting-room door: "we need the room" — the occupants wrap up. SIGTERM is the office manager's email: "please vacate by five" — formal, expected to be honored, time allowed for packing (saving files, closing connections). SIGKILL is building security cutting the power and carrying everyone out: effective, instant, and nothing gets packed — whiteboards half-erased, coffee still on.
Where the analogy stops working. Evicted humans grumble but comply eventually. A process handling SIGTERM can simply decline forever (a trap that ignores it), and no amount of repeated polite mail changes that — which is precisely why SIGKILL exists and why it is uncatchable: the system needs one guaranteed eviction that requires no cooperation. The cost of guaranteed is no cleanup, ever — Part B prices that out.
</aside>
🧪 Exercise 13.1 — polite death, rude death, and the death certificates
Both sleepers are killed on purpose.
sleep 30 & pid=$!
kill "$pid" # default: SIGTERM
wait "$pid" 2>/dev/null
echo "after TERM: wait verdict $?"
sleep 30 & pid=$!
kill -9 "$pid" # SIGKILL
wait "$pid" 2>/dev/null
echo "after KILL: wait verdict $?"
<aside> 📖
Official docs: Bash manual — Bourne Shell Builtins (documents trap) · Bash manual — Signals
</aside>
trap 'commands' SIGNAL tells bash: when that signal arrives, run these commands instead of dying by default. And bash adds one pseudo-signal that makes trap the backbone of reliable scripts: EXIT, which fires when the script ends for any reason — normal finish, exit 1, a failed guard, a caught signal. One line therefore guarantees cleanup on every path: