Memra

The interface the book never shows

◈ 10 cards

Install a signal handler with sigaction so that it stays installed, blocks the signals it must while it runs, restarts the system call it interrupted, and calls only functions that are safe to call from a handler.

The bug in signal()

Everything in Lesson 1 used signal(), because that is what almost every textbook uses. It is also the interface POSIX tells you not to write, and the reason is a race you cannot see in the source.

Under the original System V semantics, signal() is one-shot: the moment a caught signal is delivered, the kernel resets that signal’s disposition back to SIG_DFL and then calls your handler. If you want to stay caught, the handler must reinstall itself:

void on_int(int sig) {
    signal(SIGINT, on_int);
    write(1, "caught\n", 7);
}

Look at where the window is. Between the kernel resetting the disposition and the handler executing its first statement, the disposition for SIGINT is the default — terminate. A second SIGINT arriving in that window kills the process, and it does so with the reinstalling line sitting right there in the source looking correct. Move the reinstall to the end of the handler and the window grows to the whole handler.

BSD went the other way and made the disposition persist, so the same source behaved differently on different UNIX systems and the reinstalling line was either essential or redundant depending on where you compiled. That is the whole problem: signal()’s semantics are not fixed by the standard, so portable code cannot rely on either behaviour. Do not write it.

sigaction() — the interface that is fixed

POSIX defines one call that replaces signal() and specifies exactly what it does:

int sigaction(int signum, const struct sigaction *restrict act,
              struct sigaction *restrict oldact);

It returns 0 on success and −1 on failure with errno set — EINVAL if signum is invalid or is SIGKILL or SIGSTOP. act describes the disposition you want; if oldact is not NULL, the previous disposition is written there, which is how you save and restore one. Passing NULL for act and a real pointer for oldact queries the disposition without changing it.

The work is in the structure. struct sigaction has these members:

  • void (*sa_handler)(int) — the disposition itself: SIG_DFL, SIG_IGN, or the address of your handler function. This is the field that does the same job as signal()’s second argument.
  • sigset_t sa_mask — a set of additional signals to block for the duration of the handler, and only for that duration. The signal being handled is added to this set automatically, so a handler is never re-entered by its own signal unless you ask for that with SA_NODEFER.
  • int sa_flags — flags that modify delivery. SA_RESTART is the one to know; SA_NODEFER, SA_RESETHAND (which asks for the one-shot behaviour), SA_NOCLDSTOP and SA_SIGINFO are the others you will meet.
  • void (*sa_sigaction)(int, siginfo_t *, void *) — an alternative handler that receives the sending PID, the sending UID and a fault address. It is used instead of sa_handler, and only when SA_SIGINFO is set in sa_flags.

A sigset_t is opaque — you never assign to it directly. sigemptyset(&set) empties it, sigfillset(&set) fills it, and sigaddset(&set, SIGQUIT) adds one signal. sigemptyset is not optional: an uninitialised sa_mask is whatever garbage was on the stack, which can block an arbitrary set of signals for the lifetime of every handler call.

The two things sigaction gives you that signal cannot

The disposition persists. No reinstallation, no window, no platform-dependent behaviour. Installed once, the handler stays installed until you change it or call exec.

sa_mask closes the race properly. Where the reinstalling trick left a gap in which the default action applied, sa_mask blocks the named signals outright while the handler runs. A blocked signal is not lost and not delivered: it is held pending, and delivered the instant the handler returns and the mask is restored. That is the difference between "this signal is deferred" and "this signal kills me".

SA_RESTART

A slow system call is one that can block indefinitely — read on a terminal or a pipe, write to a full pipe, wait, accept, pause. When a signal is caught while the process is inside one, the historical behaviour is that the call gives up and returns −1 with errno == EINTR, and every caller has to be written to notice that and retry:

while ((n = read(fd, buf, sizeof buf)) == -1 && errno == EINTR)
    ;

Setting SA_RESTART in sa_flags asks the kernel to do that retry for you: after the handler returns, the interrupted call is resumed rather than failed. Your read simply blocks again and eventually returns data, and the EINTR retry loop disappears from every call site. This is not a universal cure — some calls are never restartable no matter what the flag says — but for the ordinary blocking read/write/wait of a course assignment it is exactly what you want.

Worked example — a program that survives Ctrl-C

volatile sig_atomic_t hits = 0;

void on_int(int sig) {
    hits++;
    write(1, "not today\n", 10);
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = on_int;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGINT, &sa, NULL) == -1) _exit(1);
    for (;;) pause();
}

Press Ctrl-C and the terminal driver generates SIGINT for the foreground process group. The kernel finds the disposition is on_int, blocks SIGINT (automatically, because it is the signal being delivered) plus everything in sa_mask (nothing extra, here), runs the handler, restores the mask, and resumes pause(). The default action — terminate — never happens, because it is no longer the disposition. Press Ctrl-C fifty times and you get fifty lines.

Now the honest part of the answer: kill -9 on that PID ends it instantly. SIGINT was survivable because its disposition could be changed. SIGKILL has no disposition to change, and the sigaction call that tried to give it one would have returned −1 with EINVAL.

If you only want the signal gone rather than noticed, set sa.sa_handler = SIG_IGN and drop the handler entirely. That is the shortest correct answer to "make Ctrl-C do nothing", and it is worth naming as an alternative in an exam answer even when you go on to write the handler version.

What a handler is allowed to call

A handler runs between two arbitrary instructions of your program — including instructions inside library functions. If the main flow was halfway through malloc, updating the free list, and the handler calls malloc too, the second call walks a data structure that is momentarily inconsistent and the heap is corrupted. The same is true for printf, which manipulates the stdio buffers, and for anything else that keeps global state.

POSIX defines a list of async-signal-safe functions that are guaranteed to be re-entrant. write, read, open, close, kill, _exit, signal and sigaction are on it. printf, fprintf, malloc, free, strdup and exit are not. So a handler writes with write(2), and if it needs to tell the main program anything it sets a volatile sig_atomic_t flag that the main loop polls. A handler that saves and restores errno around its work is being careful about the same class of bug: write inside the handler can overwrite the errno the interrupted code was about to read.

signal()sigaction()Disposition after adeliverymay reset to SIG_DFLpersistsBlock other signals in thehandlerno controlyes, via sa_maskRestart a slow call itinterruptedno controlyes, via SA_RESTARTSemantics fixed by thestandardno - System V and BSDdifferyesWhat to do with itread it in old codewrite it in new codeThe one-shot reset is the race: the default action applies until the handler reinstalls.
Four of the five rows are the reason POSIX tells you to write the right-hand column.
kernelautomaticreturnSA_RESTARTSIGINT deliveredprocess is blocked in read()mask appliedSIGINT + sa_mask blockedhandler runswrite() onlymask restoredanything pending now arrivesread() resumesbecause SA_RESTART
The mask is on for exactly stage three, and SA_RESTART is what makes stage five a resume instead of a failure with EINTR.
NORMAL ~/memra/learn/comp-325/reliable-signal-handling-with-sigaction utf-8 LF