Signals: how the OS interrupts a process
Exit codes (lesson 3-3) are a dying process's last message to its parent. Signals go the other direction: they are how the OS, or another process, interrupts a running process right now. Every Ctrl-C you have ever pressed to stop a runaway script, every clean server shutdown during a deploy, and the kill command from lesson 3-2 all work through signals — this is the standard mechanism for stopping anything on macOS or Linux, and you will use it within your first week of running real software.
A signal is a small numbered message the kernel delivers to a process. The process does not check for it; the kernel interrupts the process mid-run. The process can register a handler — a function of its own that runs when a given signal arrives — or accept the default effect. The signals you will actually meet:
| Signal | Number | Usually sent by | Default effect |
|---|---|---|---|
SIGINT | 2 | Ctrl-C in the terminal | terminate |
SIGTERM | 15 | kill <pid>, deploy tools | terminate, but a handler may clean up first |
SIGKILL | 9 | kill -9 <pid> | terminate immediately; cannot be caught or ignored |
Now the two kill commands from lesson 3-2 read precisely: kill 4021 sends SIGTERM, a request the program may handle to finish cleanly. kill -9 4021 sends SIGKILL, and the kernel simply erases the process — no handler, no cleanup, no goodbye.
Code exercise · python
Run this. signal.signal registers a handler function for SIGTERM, and os.kill sends a signal (despite the name, it can send any signal, not just fatal ones). Here the process signals itself: the handler runs, and because SIGTERM was handled, the process survives.
Code exercise · python
Run this. The parent starts a child that would sleep for 60 seconds, then terminates it with proc.terminate(), which sends SIGTERM. When a signal kills a child, subprocess reports the exit code as MINUS the signal number, so the parent can tell "exited with an error" apart from "was killed".
The graceful-shutdown pattern
Why does SIGTERM allow a handler while SIGKILL does not? Because both needs are real. Production servers register a SIGTERM handler that stops accepting new work, finishes the requests already in flight, and saves state before exiting — killing a database mid-write without this is how data gets corrupted. But if a process is stuck or malicious, the system still needs a way to remove it that the process cannot veto: that is SIGKILL, enforced by the kernel alone.
Deploy systems bake this in. Docker and Kubernetes stop a container by sending SIGTERM, waiting a grace period (10-30 seconds), and only then sending SIGKILL. If a service you ship ignores SIGTERM, every deploy ends with it being shot mid-request. Handling SIGTERM is not optional polish; it is expected of real services.
Code exercise · python
Your turn: build the graceful-shutdown skeleton. Register a handler for SIGTERM that sets shutting_down to True (use the global keyword). The signal is then sent, and the final if-block should print the shutdown message. Match the expected output exactly.
Quiz
A deploy tool stops your service with SIGTERM, waits 15 seconds, then sends SIGKILL if it is still alive. Why this two-step dance instead of SIGKILL immediately?