Skip to content
Signal Handling
step 1/5

Reading — step 1 of 5

Read

~3 min readProduction Concerns

Signal Handling

Press Ctrl-C and the running command dies — but your shell survives, prints a fresh prompt, carries on. That selective survival is not an accident; it's the shell doing deliberate signal management, and it's the difference between a shell and a toy that dies with its first interrupted child.

Signals: asynchronous nudges

A signal is the kernel's way of interrupting a process with a numbered event, whenever it likes, whatever the process was doing:

SIGINT  (2)   Ctrl-C — "please stop"          — catchable
SIGTSTP (20)  Ctrl-Z — "please pause"          — catchable (job control!)
SIGCHLD (17)  "one of your children changed state"
SIGTERM (15)  polite kill                       — catchable
SIGKILL (9)   impolite kill                     — NOT catchable, ever
SIGSEGV (11)  you dereferenced garbage (C course alumni know)

A process can set a disposition per signal: default (usually die), ignore, or run a handler function. SIGKILL accepts no disposition — the kernel's guarantee that anything can always be stopped (which is why kill -9 is the last resort and why nothing you build should depend on catching it).

Who gets Ctrl-C? The foreground story

Here's the machinery behind the everyday magic. The terminal delivers Ctrl-C not to one process but to the foreground process group — every process in the currently-foreground pipeline (cat | grep | wc is three processes, one group; Ctrl-C kills all three, which is obviously what you want).

The shell's survival trick, then, has two parts:

  1. The shell ignores SIGINT in itself — an interactive shell dying on Ctrl-C would be absurd, so it sets SIGINT to ignore (and on receiving one anyway — say, at an empty prompt — just prints a new prompt line).
  2. Children get defaults back. In the fork/exec gap (that gap again!), before exec, the child resets SIGINT to default — because dispositions survive fork AND exec, so a child inheriting "ignore SIGINT" would be immune to Ctrl-C. This is a real, famous bug class: shell scripts you can't interrupt because some parent ignored the wrong signal and everything downstream inherited it. Set-ignore-in-parent, reset-in-child: the two lines that make interruption work correctly for every command your shell ever runs.

The shell then wait()s, sees the child "exited on signal 2" (distinct, in wait's status encoding, from "exited normally with code 2" — statuses carry how you died), records status 130 (128+signal, the convention), prompts anew.

SIGCHLD and not blocking

When a child changes state, the parent gets SIGCHLD. A simple shell doesn't need it — it waits synchronously right after forking. It becomes essential the moment you add background jobs (sleep 60 &, next lessons): the shell can't sit in wait() — it's busy prompting you — so it reaps finished children on SIGCHLD instead, preventing zombie buildup. File the pattern: synchronous wait for foreground, SIGCHLD-driven reaping for background — that split is job control's foundation.

Your exercise: Signal Dispatch State Machine

Given processes with dispositions (default / ignore / handler) and a stream of delivered signals, track who dies, who ignores, who handles — including the inheritance rules across fork/exec and the uncatchability of SIGKILL. The graded subtleties are exactly the real ones: dispositions inherited on fork (ignore stays ignore — the immune-child bug, simulated), handler resets across exec (handlers can't survive exec — the handler function's code no longer exists! — so handled signals revert to default, while ignored ones stay ignored: the asymmetry everyone forgets), and 128+N status encoding. Small machine, deep waters — and every rule in it is one your shell must apply in the two lines before every exec.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…