Software interrupts, and the three things you can do about one
◈ 10 cardsName a signal by its symbolic name, state the kernel default action for it, and set its disposition to ignore, to the default, or to a handler you wrote — including the two signals for which none of that is allowed.
A signal is a one-bit message with a name
A signal is the kernel telling a process that something happened. It carries no payload — no string, no number you chose, no return address. All it carries is which signal it is, and every signal has a symbolic name defined in <signal.h>. That is the whole channel, and its poverty is the point: a signal is the cheapest possible asynchronous notification, and it is what UNIX uses when there is no time or no context for anything richer.
Signals arrive from four places, and knowing which is which is half of every exam question about them:
- The terminal driver, when the user hits a key it treats specially.
Ctrl-CgeneratesSIGINT,Ctrl-\generatesSIGQUIT,Ctrl-ZgeneratesSIGTSTP. Note the wording: the key does not kill anything. It asks the driver to generate a signal, and the signal is delivered to every process in the terminal’s foreground process group. - The hardware, via the kernel, when the process does something illegal. Dereference a bad pointer and you get
SIGSEGV; divide by zero and you getSIGFPE. - Another process, by calling
kill(2)— or a user at a shell prompt runningkill(1), which is a thin wrapper over it. The name is unfortunate:killsends any signal, and most of them do not kill. - The process itself.
alarm(10)asks the kernel to send this processSIGALRMin ten seconds.abort()raisesSIGABRT. Writing into a pipe nobody is reading earnsSIGPIPE. And when a child terminates, the parent getsSIGCHLD.
The three dispositions
For each signal, a process has a disposition: what should happen when this signal is delivered. There are exactly three choices, and signal() (or, properly, sigaction() — Lesson 2) is how you pick one:
- The kernel default, spelled
SIG_DFL. Every signal has one, defined by the standard. For most it is terminate the process; forSIGQUITandSIGSEGVit is terminate and dump core; forSIGCHLDit is ignore; forSIGSTOPandSIGTSTPit is stop the process untilSIGCONTarrives. - Ignore it, spelled
SIG_IGN. The signal is still generated and still delivered — it is simply discarded on arrival, and the process never notices. - Catch it, by giving the address of a function. When the signal arrives the kernel suspends whatever the process was doing, runs your function with the signal number as its argument, and — if the function returns — resumes the process at the instruction it interrupted.
A disposition is per-process and per-signal. Nothing is global, and a fresh fork inherits the parent’s dispositions while exec resets every caught signal back to the default (a handler’s address means nothing in a new program image).
The two exceptions
SIGKILL and SIGSTOP can be neither caught, nor ignored, nor blocked. An attempt to install any disposition for them fails — sigaction returns −1 with errno set to EINVAL. This is deliberate, and it is the answer to "can I write a program that cannot be killed": no. The system administrator must retain one signal that removes a process no matter what the process wants, and one that stops it, or a runaway program would be unkillable by design. kill -9 is SIGKILL, and it is why it always works.
Worked example — one program, three signals
Here is a program that gives three signals three different dispositions, then loops forever. Read it once before the trace below.
volatile sig_atomic_t ticks = 0;
void on_int(int sig) { write(1, "SIGINT caught, still here\n", 26); }
void on_alrm(int sig) { ticks++; alarm(10); }
int main(void) {
signal(SIGHUP, SIG_IGN);
signal(SIGINT, on_int);
signal(SIGALRM, on_alrm);
alarm(10);
for (;;) pause();
}
Start it in one terminal and note its PID — say 4210. Now, from a second terminal:
kill -HUP 4210 — the kernel generates SIGHUP, delivers it, sees the disposition is SIG_IGN, and discards it. The program prints nothing and keeps running. Nothing is lost and nothing is queued: an ignored signal is simply gone.
kill -INT 4210 — the same as the user pressing Ctrl-C in the program’s own terminal. The disposition is on_int, so pause() is interrupted, on_int runs and writes its line, and then the program resumes. The program has survived an interrupt it would ordinarily have died from, because the default action for SIGINT is termination and we replaced it.
kill -KILL 4210 — the process disappears. No line is printed, no cleanup runs, no handler is consulted, because SIGKILL has no disposition to consult. The signal(SIGKILL, ...) call the program could have made would have failed, and even if it had been written it would have changed nothing.
Meanwhile, every ten seconds, SIGALRM arrives from the process’s own alarm() timer, on_alrm bumps a counter and re-arms the timer. That re-arming is necessary: alarm() is one-shot. A second alarm() supersedes a pending one, and alarm(0) cancels it outright.
Why the handler variable is volatile sig_atomic_t
A handler runs between two instructions of the main program, so any variable both of them touch is shared across an interruption the compiler cannot see. volatile stops the compiler caching the value in a register across the loop; sig_atomic_t is the one integer type the standard promises can be read and written in a single uninterruptible step. Every other kind of sharing between a handler and the main flow is a race.