<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>

Part A — Signals: the messages processes send

A1. A signal is a numbered tap on the shoulder

<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 $?"

Interview questions — Part A

Part B — trap: your script's affairs, in order

B1. The EXIT trap: cleanup that always runs

<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: