Memra

Software interrupts, and the three things you can do about one

◈ 10 cards

Name 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-C generates SIGINT, Ctrl-\ generates SIGQUIT, Ctrl-Z generates SIGTSTP. 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 get SIGFPE.
  • Another process, by calling kill(2) — or a user at a shell prompt running kill(1), which is a thin wrapper over it. The name is unfortunate: kill sends any signal, and most of them do not kill.
  • The process itself. alarm(10) asks the kernel to send this process SIGALRM in ten seconds. abort() raises SIGABRT. Writing into a pipe nobody is reading earns SIGPIPE. And when a child terminates, the parent gets SIGCHLD.

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:

  1. The kernel default, spelled SIG_DFL. Every signal has one, defined by the standard. For most it is terminate the process; for SIGQUIT and SIGSEGV it is terminate and dump core; for SIGCHLD it is ignore; for SIGSTOP and SIGTSTP it is stop the process until SIGCONT arrives.
  2. Ignore it, spelled SIG_IGN. The signal is still generated and still delivered — it is simply discarded on arrival, and the process never notices.
  3. 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.

SignalHow it arrivesDefault actionCatchable?SIGHUPterminal hangup;daemon reloadconventionterminateyesSIGINTCtrl-C at theterminalterminateyesSIGQUITCtrl-\ at theterminalterminate + coreyesSIGKILLkill -KILL onlyterminateNOSIGSEGVinvalid memoryreferenceterminate + coreyesSIGALRMthe process ownalarm() timerterminateyesSIGTERMkill with no signalnamedterminateyesSIGCHLDa child stopped orterminatedignoreyesSIGSTOPkill -STOP onlystop the processNOSIGTSTPCtrl-Z at theterminalstop the processyesSIGPIPEwrite to a pipewith no readerterminateyesNames, not numbers - the numbers differ between Linux and BSD/Solaris.
The last column is the whole of the SIGKILL/SIGSTOP exam question: two rows say no, and every other row says yes.
NORMAL ~/memra/learn/comp-325/signals-names-dispositions-and-delivery utf-8 LF