Course outline · 0% complete

0/28 lessons0%

Course overview →

Signals: how the OS interrupts a process

lesson 3-4 · ~10 min · 10/28

Signals, how the OS interrupts a process

Exit codes from 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 pressed to stop a runaway script, every clean server shutdown during a deploy, and the kill command from lesson 3-2 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.

SignalNumberUsually sent byDefault effect
SIGINT2Ctrl-C in the terminalterminate
SIGTERM15kill <pid>, deploy toolsterminate, but a handler may clean up first
SIGKILL9kill -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 with no handler, no cleanup, and no goodbye.

Catching a signal a process sends to itself

A handler registered for SIGTERM turns a fatal default into a printed line.

import signal
import os

def handler(signum, frame):
    print("got signal", signum)

signal.signal(signal.SIGTERM, handler)
os.kill(os.getpid(), signal.SIGTERM)
print("still running")

Output

got signal 15
still running

signal.signal registers a handler function for a given signal, and os.kill sends one. Despite the name, os.kill can send any signal rather than only fatal ones, and here the process signals itself.

The number 15 is SIGTERM's number from the table above. Without the handler the default effect would have terminated the process before the last print ever ran, which is the entire difference a handler makes.

Terminating a child and reading the negative code

The parent starts a child that would sleep for 60 seconds, then stops it early.

import subprocess
import sys
import time

proc = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"])
time.sleep(0.5)
proc.terminate()
code = proc.wait()

print("exit code:", code)
print("negative means killed by a signal:", code < 0)
print("signal number:", -code)

Output

exit code: -15
negative means killed by a signal: True
signal number: 15

proc.terminate() sends SIGTERM. When a signal kills a child, subprocess reports the exit code as the negative of the signal number, so the parent can tell "exited with an error" apart from "was killed".

The child never got to finish its sleep, because the kernel delivered SIGTERM and, with no handler registered, applied the default effect. Calling proc.kill() instead would send SIGKILL, number 9, and the reported code would be -9.

The graceful-shutdown pattern

SIGTERM allows 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 that step 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.

SignalCatchablePurpose
SIGTERMyesask politely, allow cleanup
SIGKILLnoguarantee removal

Deploy systems bake this in. Docker and Kubernetes stop a container by sending SIGTERM, waiting a grace period of roughly 10 to 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.

SIGTERM sentgrace period, 10 to 30 secondsstop taking work, finish in-flight requests, flush and saverunningSIGKILL, if still alivecannot be caught, no cleanup
A deploy sends SIGTERM, waits out a grace period, and only then forces the issue.

A graceful-shutdown skeleton

The handler sets a flag rather than doing the work, and the main flow acts on the flag.

import signal
import os

shutting_down = False

def handler(signum, frame):
    global shutting_down
    shutting_down = True

signal.signal(signal.SIGTERM, handler)

os.kill(os.getpid(), signal.SIGTERM)

if shutting_down:
    print("finishing current work, then exiting cleanly")

Output

finishing current work, then exiting cleanly

The handler needs global shutting_down before assigning, otherwise Python creates a local variable and the flag never changes. That single missing line is the most common reason a shutdown handler appears to do nothing.

Setting a flag is the recommended shape for a real service. A handler runs at an arbitrary point mid-instruction, so doing heavy work inside it is risky, while flipping a boolean and letting the normal loop notice is safe.

Why deploys use SIGTERM before SIGKILL

A deploy tool that sends SIGTERM, waits 15 seconds, and only then sends SIGKILL is buying the best of both behaviors.

SIGTERM gives the process a chance to finish in-flight work and save state through its handler. SIGKILL is the fallback that cannot be vetoed, for processes that do not exit.

SIGTERM is catchable precisely so services can shut down cleanly: stop taking requests, finish current ones, flush and save. SIGKILL exists because the system must retain a way to remove any process regardless of what its code does, and the kernel enforces it with no handler running at all.

PhaseSignalOutcome for a well-behaved service
immediatelySIGTERMbegins draining, stops new work
within the grace periodnonefinishes and exits on its own
after the grace periodSIGKILLnever reached

The grace period is the contract between the two. Sending SIGKILL first would guarantee removal but also guarantee dropped requests, and sending only SIGTERM would leave a hung process alive forever.