Proven C Book한국어 GitHub

76 Signals — <signal.h>

What to know first

chapter 75, Diagnostics and control · errno and the handler’s restrictions
chapter 53, The three faces of main · a program’s start and end
chapter 3, Programs and processes · the process as a container

Looking back

Chapter 75 said a signal handler may not call printf or malloc, and that about all it may do is assign to a volatile sig_atomic_t. But a signal ultimately calls a function inside my own program — why are the restrictions so severe?

A. Because there is no telling when it will cut in. A signal does not respect the boundaries of functions. It can arrive while malloc has half-rewritten its free list, or while printf has written half of a buffer. Call the same function again from a handler in that state and the data structure breaks.

A signal, in other words, is a third thing — neither a thread nor a function call. It stops the current flow, cuts in, and returns, so there is no way to know “what is half-done right now”. Every rule in this chapter follows from that one sentence.

The need for this chapter, and its context

Chapter 75 surveyed four headers; two of them deserve a close reading of their own. Signals come first because they are flow arriving from outside, while the next chapter’s setjmp is flow jumping from inside — the two make a contrast. The pair reads as “the two directions in which flow is cut”.

By the end of this chapter

A close reading of one header, <signal.h>. Where it came from (a Unix inheritance), the exact shape of its two functions with their arguments and return values, what sig_atomic_t really is, the list of what a handler may do, why memory cannot be allocated inside one, how the kernel saves and restores registers, POSIX’s sigaction and the inside of its structures, and how servers, the JVM and garbage collectors actually use signals.

The questions this chapter answers

  1. Why did the standard not take the better sigaction?
  2. Then what do you do when a handler really must record something?
  3. Does setjmp/longjmp not do the same thing?
  4. In a program with several threads, where does a signal go?

76.1 Where it came from — a Unix inheritance

C did not invent signals. Unix made them in the 1970s as “the cheapest way to tell a process something”, and the C standard took only the minimum that would hold anywhere.

Early Unix signals had a famous flaw. Once a handler ran, the disposition immediately reverted to the default (so the first line of a handler had to re-install itself), and if the same signal arrived in that gap the program died. Because of that window, the signals of the time were called unreliable signals.

4.2BSD produced a new interface that fixed this (the sigvec family), and that design was tidied into POSIX’s sigaction. What the 1989 C standard took, however, was not the fixed one but the common denominator — which is why the old window is still in the standard’s signal today.

Q. Why did the standard not take the better sigaction?

A. Because of C’s long-standing principle: take only what holds where there is no operating system (chapter 62). sigaction stands on operating-system notions — processes, signal masks, restarting system calls. C must run on embedded chips that have none of those, so the standard fixed only “if there is such a thing as a signal, this much exists.”

So this chapter is in two layers. The standard layer (works anywhere, with gaps) and the POSIX layer (no gaps, but only on Unix-like systems). Practical Unix code uses the latter almost without exception.

76.2 Two functions — the exact shape

The standard defines only two.

DeclarationWhat it does
void (*signal(int sig, void (*func)(int)))(int);Sets the disposition of signal sig to func, and returns the previous one
int raise(int sig);Sends signal sig to the caller itself

Table 77.1

Why signal’s declaration is rough was read by procedure in chapter 60 — “a function taking a signal number and a handler, returning the previous handler.”

76.2.1 signal’s arguments and return value

PlaceWhat you giveMeaning
sigA signal numberOne of the standard six, or a value the implementation defines
funcSIG_DFLBack to default handling (mostly termination)
funcSIG_IGNIgnore — the signal arrives and nothing happens
funcA function pointerUse that function as the handler
returnThe previous dispositionSIG_DFL, SIG_IGN or the previous handler
returnSIG_ERRSetting failed; errno then holds the reason

Table 77.2

Not discarding the return value is the first discipline. Miss a failure and the program dies quietly when the signal finally arrives.

76.2.2 raise’s argument and return value

raise(sig) sends the signal to the caller. It returns 0 on success and non-zero on failure. If a handler is installed it runs before raise returns — that is, synchronously.

That is exactly what abort does: it raises SIGABRT, and terminates abnormally if there is no handler or the handler returns (chapter 67).

examples-en/ch76/sig_basic.c

/* The standard part of <signal.h> — three dispositions and raise. */
#include <signal.h>
#include <stdio.h>
#include <string.h>

/* This is all the standard permits in a handler's body: assigning to a
   volatile sig_atomic_t. Anything else is outside the contract. */
static volatile sig_atomic_t got_int;
static volatile sig_atomic_t got_term;

static void on_signal(int sig)
{
    if (sig == SIGINT)  got_int  = 1;
    if (sig == SIGTERM) got_term = 1;
    /* no printf here — the text explains why */
}

static const char *disposition(void (*h)(int))
{
    if (h == SIG_DFL) return "SIG_DFL (default handling)";
    if (h == SIG_IGN) return "SIG_IGN (ignore)";
    if (h == SIG_ERR) return "SIG_ERR (setting failed)";
    return "my handler";
}

int main(void)
{
    /* -- (1) install — what comes back is the *previous* disposition ---- */
    void (*prev)(int) = signal(SIGINT, on_signal);
    printf("signal(SIGINT, on_signal) returned %s\n", disposition(prev));

    /* -- (2) raise — send a signal to yourself -------------------------- */
    printf("before raise(SIGINT): got_int = %d\n", (int)got_int);
    int r = raise(SIGINT);
    printf("raise returned %d (0 means success), got_int = %d\n", r, (int)got_int);

    /* -- (3) ignore — with SIG_IGN the signal simply vanishes ----------- */
    (void)signal(SIGTERM, SIG_IGN);
    (void)raise(SIGTERM);
    printf("after SIG_IGN, raise(SIGTERM): got_term = %d (still 0)\n",
           (int)got_term);

    /* -- (4) does the handler survive? — implementation-defined (§7.14.1.1) */
    void (*was)(int) = signal(SIGINT, prev);
    printf("\nafter handling one signal, what was installed: %s\n", disposition(was));
    printf("  -> this implementation resets to SIG_DFL (the old System V way).\n");
    printf("  -> the standard allows either, so portable code re-installs in\n");
    printf("     the handler, or uses POSIX sigaction.\n");

    /* -- (5) the standard defines only six signals ---------------------- */
    struct { int num; const char *name; const char *meaning; } table[] = {
        { SIGABRT, "SIGABRT", "abnormal termination (abort)" },
        { SIGFPE,  "SIGFPE",  "arithmetic error (divide by zero, ...)" },
        { SIGILL,  "SIGILL",  "invalid instruction" },
        { SIGINT,  "SIGINT",  "interactive attention (Ctrl+C)" },
        { SIGSEGV, "SIGSEGV", "invalid memory access" },
        { SIGTERM, "SIGTERM", "termination request" },
    };
    printf("\nthe %zu signals the standard defines:\n", sizeof table / sizeof *table);
    for (size_t i = 0; i < sizeof table / sizeof *table; i++)
        printf("  %-8s = %2d   %s\n", table[i].name, table[i].num, table[i].meaning);

    printf("\nsizeof(sig_atomic_t) = %zu bytes\n", sizeof(sig_atomic_t));
    return 0;
}

Output

signal(SIGINT, on_signal) returned SIG_DFL (default handling)
before raise(SIGINT): got_int = 0
raise returned 0 (0 means success), got_int = 1
after SIG_IGN, raise(SIGTERM): got_term = 0 (still 0)

after handling one signal, what was installed: SIG_DFL (default handling)
  -> this implementation resets to SIG_DFL (the old System V way).
  -> the standard allows either, so portable code re-installs in
     the handler, or uses POSIX sigaction.

the 6 signals the standard defines:
  SIGABRT  =  6   abnormal termination (abort)
  SIGFPE   =  8   arithmetic error (divide by zero, ...)
  SIGILL   =  4   invalid instruction
  SIGINT   =  2   interactive attention (Ctrl+C)
  SIGSEGV  = 11   invalid memory access
  SIGTERM  = 15   termination request

sizeof(sig_atomic_t) = 4 bytes

The demonstration shows four things in the flesh. The previous value comes back (first line), raise calls the handler on the spot (second), SIG_IGN makes the signal vanish (third), and the last one matters — this implementation resets to SIG_DFL right after handling.

76.2.3 The six signals the standard defines

NameWhen it arrivesDefault action
SIGABRTA call to abort()Abnormal termination
SIGFPEAn arithmetic error (divide by zero, overflow, …)Abnormal termination
SIGILLExecuting an invalid instructionAbnormal termination
SIGINTInteractive attention (usually Ctrl+C)Termination
SIGSEGVAn invalid memory accessAbnormal termination
SIGTERMA termination requestTermination

Table 77.3

The standard does not fix the numeric values — the 2 and 15 the demonstration printed belong to this implementation. The rule is use the names, never the numbers.

76.3 What a handler may do

The most important section in this chapter. The standard (§7.14.1.1) settles what is permitted inside a handler by enumeration. Outside that list is undefined behaviour.

What is permittedCondition
Assigning a value to a volatile sig_atomic_t objectAssigning, not reading
Handling lock-free atomic objects<stdatomic.h>, when lock-free (chapter 80)
Calling abort
Calling _Exit
Calling quick_exit
Calling signalOnly with the signal number that invoked it

Table 77.4

Every other standard library function is forbidden — printf, malloc, strlen, even exit. And touching objects with static or thread storage duration is forbidden too unless it falls under the first row.

A common misconception. “Logging with printf in a handler is convenient”

The most common and longest-lived accident. printf is a function with internal buffers and locks, so cutting into its intermediate state breaks its data structures. On a good day the output interleaves; on a bad one it deadlocks — a signal arrives while printf holds a lock, the handler calls printf again, and it waits for a lock it holds itself.

Worse, it mostly works. So the bug passes the tests, ships, and shows up as an occasional freeze under load.

If something really must be printed from a handler on Unix, use write — a function POSIX separately guarantees to be async-signal-safe. The second demonstration does exactly that.

76.3.1 What sig_atomic_t really is

sig_atomic_t is an integer type whose value never appears half-written even when a signal cuts in. Why does such a type need to exist? Because a large integer may be stored in two steps on some machines, and a signal arriving in between would see a half-changed value.

volatile is there for a different reason. The compiler may decide “nothing in this loop changes stop” and delete the test altogether (chapter 13′s optimisation). volatile says really read it every time. The two do different jobs, so both are needed — volatile sig_atomic_t.

WhatWhat it preventsWithout it
sig_atomic_tSeeing a half-written valueA partially updated value can be read
volatileThe compiler eliding the readThe loop never sees the flag

Table 77.5

Since C11, lock-free atomic types such as atomic_int may also be used in a handler (chapter 80). In a program with several threads that is the more accurate choice — sig_atomic_t guarantees only signal versus main flow, not thread against thread.

76.4 Handlers and memory — why malloc is not on the list

The most keenly felt absence from the permitted list is memory allocation. To record anything inside a handler you need a vessel, and neither malloc nor free may be called. Seeing why at the machine level makes the rule stick.

An allocator manages a data structure called the free list (chapter 45). One malloc is a multi-step update that detaches a piece from that list and rewrites the links of its neighbours. There is necessarily a moment when the update is half done, and a signal can cut in at exactly that moment.

The moment it cuts inIf the handler calls malloc
The free-list links are half rewrittenIt follows a half-rewritten list and hands out the wrong piece
Just before the block’s size is writtenA later free returns it with the wrong size
While the allocator’s lock is heldIt waits for a lock it holds itself — deadlock

Table 77.6

The third row is the nastiest. Modern allocators keep an internal lock against being called from several threads at once. Let the thread holding that lock take a signal, and let the handler call malloc again, and the lock is never released. The program does not die; it stops — the hardest kind of accident to diagnose.

There is one worse combination. Escaping from a handler with longjmp (chapter 77). Here the trouble comes without calling malloc again — because you jump out still holding the lock. From then on the main flow’s first malloc hangs. Half the reason chapter 77 calls jumping from a handler dangerous is this.

Q. Then what do you do when a handler really must record something?

A. Take it in advance. Allocate the vessel you need before installing the handler, and let the handler only write into it. A static array is better still — there is no allocation at all.

static char report[4096];               /* obtained up front */
static volatile sig_atomic_t report_len;

If the amount to record is not bounded, it was never a job for a handler. Raise a flag and hand it to the main flow. If something truly must be recorded now — a crash report, where the next moment is certain death — put it in a pre-allocated buffer and push it out with write. That is what Redis’s crash report, seen later, does.

76.5 What a signal saves and restores — the register context and errno

A handler cuts in halfway through a function and returns to exactly that place, even though half-computed values are scattered across the registers. How?

Because the operating system saves every register and restores them. When delivering a signal the kernel builds a signal frame on the stack and puts the whole current register set into it (on Linux, ucontext_t is that vessel). When the handler returns, sigreturn puts those values back. So whichever instruction boundary it cut in at, the computation carries on.

examples-en/ch76/sig_context.c

/* what a signal saves and restores — the register context, and errno. */
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static volatile sig_atomic_t hits;

/* ── (1) a handler that does not look after errno ──────────────────
   write is one of the few functions a handler may call, but on failure it
   changes errno. If the main flow was about to read that value, the whole
   diagnosis is turned upside down. */
static void careless(int sig)
{
    (void)sig;
    hits++;
    ssize_t n = write(-1, "", 1);   /* always fails — errno = EBADF */
    (void)n;
}

/* ── (2) a handler that saves and restores errno ─────────────────── */
static void careful(int sig)
{
    int saved = errno;              /* saved on the way in */
    (void)sig;
    hits++;
    ssize_t n = write(-1, "", 1);
    (void)n;
    errno = saved;                  /* restored on the way out */
}

/* ── a computation living in registers. same result if a signal cuts in? ── */
static long compute(int interrupt_at)
{
    long acc = 0;
    for (int i = 1; i <= 1000; i++) {
        acc += (long)i * i % 7;
        if (i == interrupt_at) raise(SIGUSR1);
    }
    return acc;
}

static int fails(void)              /* a failure that leaves errno as ENOENT */
{
    errno = 0;
    return access("/no/such/file/here", F_OK);
}

int main(void)
{
    /* (1) errno gets clobbered */
    signal(SIGUSR1, careless);
    (void)fails();
    int before = errno;
    raise(SIGUSR1);
    printf("handler that ignores errno: before %d(%s) -> after %d(%s)\n",
           before, strerror(before), errno, strerror(errno));

    /* (2) saved and restored */
    signal(SIGUSR1, careful);
    (void)fails();
    before = errno;
    raise(SIGUSR1);
    printf("handler that saves errno:   before %d(%s) -> after %d(%s)\n",
           before, strerror(before), errno, strerror(errno));

    /* (3) the result is the same even when a signal cuts in.
       This implementation reverts to SIG_DFL after handling (chapter 76),
       so the handler is installed again. */
    signal(SIGUSR1, careful);
    hits = 0;
    long quiet = compute(0);            /* without a signal */
    long hit   = compute(500);          /* hit once, right in the middle */
    printf("\ncomputed quietly            = %ld\n", quiet);
    printf("computed with a signal at 500 = %ld  (handler ran %d time)\n",
           hit, (int)hits);
    puts(quiet == hit ? "the same - the kernel saved and restored every register"
                      : "different - this should not happen");

    puts("\nHere is the difference from setjmp/longjmp (chapter 77):");
    puts("  signal:  the kernel saves and restores the *whole* register set ->");
    puts("           returning into the middle of an expression just works.");
    puts("  longjmp: jmp_buf holds only the *callee-saved* registers ->");
    puts("           locals living in the others revert to their old values.");
    return 0;
}

Output

handler that ignores errno: before 2(No such file or directory) -> after 9(Bad file descriptor)
handler that saves errno:   before 2(No such file or directory) -> after 2(No such file or directory)

computed quietly            = 2002
computed with a signal at 500 = 2002  (handler ran 1 time)
the same - the kernel saved and restored every register

Here is the difference from setjmp/longjmp (chapter 77):
  signal:  the kernel saves and restores the *whole* register set ->
           returning into the middle of an expression just works.
  longjmp: jmp_buf holds only the *callee-saved* registers ->
           locals living in the others revert to their old values.

The third part of the demonstration confirms it. A signal taken in the middle of a thousand-round computation (at round 500) leaves the result equal to the one computed undisturbed.

Q. Does setjmp/longjmp not do the same thing?

A. No. This is the decisive difference between the two devices.

A signallongjmp (chapter 77)
Who savesThe kernelThe setjmp macro
What is savedEvery registerOnly the callee-saved ones (eight slots)
Where it returnsThe very instruction interruptedThe place that called setjmp
Local variablesAll intactNo guarantee unless volatile

Table 77.7

A signal, then, is a complete context switch, and longjmp is a partial restoration. That is why a handler may return into the middle of an expression while longjmp may only be used in the four contexts the standard fixes (chapter 77).

76.5.1 errno is the exception — you must look after it yourself

If the kernel looks after the registers, what is left? State at the C level. The one most often hit is errno.

A handler that calls nothing but write still changes errno when that call fails. If the main flow was about to read the reason for a failed call, the value it reads is the one the signal left behind.

The first part of the demonstration shows it in the flesh — errno, which was ENOENT (2) before the signal, came back as EBADF (9) after the handler. An attempt to open a missing file was turned into a bad-file-descriptor error.

The prescription is two lines. Save on the way in, restore on the way out.

static void on_signal(int sig) {
    int saved = errno;          /* first line */
    /* … raise a flag, write, … */
    errno = saved;              /* last line */
}

Code that omits it mostly works — the fault shows only when a signal happens to arrive at that moment. So it is better kept as a discipline.

Platform note. Where the stack goes — the red zone and an alternate stack

The kernel builds the signal frame on the current stack. Two pieces of practical knowledge follow.

First, the x86-64 SysV convention has a 128-byte red zone below the stack pointer that a function may use without growing the stack. The kernel skips that zone when building a signal frame — otherwise it would overwrite the interrupted function’s temporaries. The practice of building kernel code with -mno-red-zone comes from here.

Second, if the SIGSEGV came from the stack overflowing, there is no stack on which to build the handler either. So POSIX provides sigaltstack to register a separate stack for handlers, used by passing SA_ONSTACK. Tools that diagnose stack overflow stand on this device.

76.6 The working pattern — raise a flag and return at once

Given so short a permitted list, handlers in practice converge on one shape.

examples-en/ch76/sig_flag.c

/* A handler has one job — raise a flag and return at once. */
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>          /* write — one of the few things a handler may call */

static volatile sig_atomic_t stop_requested;
static volatile sig_atomic_t reload_requested;
static volatile sig_atomic_t last_signal;

static void on_signal(int sig)
{
    last_signal = sig;
    if (sig == SIGTERM || sig == SIGINT) stop_requested = 1;
    if (sig == SIGUSR1)                  reload_requested = 1;

    /* That is as far as the standard permits. The write below is allowed only
       because POSIX separately guarantees it async-signal-safe (see the text). */
    static const char note[] = "  [handler] flag set\n";
    ssize_t n = write(STDOUT_FILENO, note, sizeof note - 1);
    (void)n;
}

/* The skeleton of a server — work, look at the flags, decide */
static void serve(void)
{
    int served = 0;
    for (;;) {
        if (reload_requested) {
            reload_requested = 0;
            printf("  reloading configuration (SIGUSR1)\n");
            fflush(stdout);
        }
        if (stop_requested) {
            printf("  cleaning up and going down (after serving %d)\n", served);
            return;
        }
        served++;
        if (served == 2) (void)raise(SIGUSR1);   /* simulate a reload request */
        if (served == 4) (void)raise(SIGTERM);   /* simulate a shutdown request */
    }
}

int main(void)
{
    /* This implementation resets to SIG_DFL after handling, so we re-install.
       (Which is why POSIX code uses sigaction — the next example.) */
    if (signal(SIGUSR1, on_signal) == SIG_ERR) return 1;
    if (signal(SIGTERM, on_signal) == SIG_ERR) return 1;

    puts("server loop starting");
    fflush(stdout);   /* the handler's write bypasses the buffer — keep the order */
    serve();
    printf("last signal received = %d\n", (int)last_signal);

    /* What the handler left behind is one flag — the work happens out here.
       Printing, closing files and freeing all belong in this place. */
    puts("cleanup done outside the loop");
    return 0;
}

Output

server loop starting
  [handler] flag set
  reloading configuration (SIGUSR1)
  [handler] flag set
  cleaning up and going down (after serving 4)
last signal received = 15
cleanup done outside the loop

The handler only raises a flag. Judgement and cleanup belong to the main flow. The demonstration’s serve is that structure — it loops, sees the flags, and either reloads its configuration or cleans up and goes down. Printing, closing files and freeing all happen inside the loop.

Three things make the pattern good. It is safe — the handler does one assignment, so it cannot leave the permitted list. The moment is yours — the main flow decides whether to finish the current request first. It is testable — raise the flag directly and the same path can be exercised without any signal.

In practice. Why the output order looked reversed

The first run of the demonstration printed the handler’s output before the puts. Not a bug but buffering — printf and puts accumulate in the stdout buffer and flush later, while the handler’s write goes straight out, bypassing it (chapter 64′s buffering).

That small observation re-explains the misconception above. The handler and the main flow share a buffer but do not write by the same rules. So the demonstration used fflush to line the order up, and practice avoids printing from handlers altogether.

76.7 POSIX’s sigaction — filling the standard’s gaps

What Unix-like systems actually use is sigaction. It fills exactly three gaps in the standard signal.

GapStandard signalsigaction
Does the handler survive?Implementation-defined (it vanished in the demonstration)It stays, unless SA_RESETHAND is given
If the same signal arrives while handlingImplementation-definedBlocked by default; sa_mask blocks more
Interrupted system callsNot settledRestarted automatically with SA_RESTART

Table 77.8

examples-en/ch76/sig_action.c

/* POSIX sigaction — filling the gaps the standard signal left. */
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static volatile sig_atomic_t hits;
static volatile sig_atomic_t last_code;
static volatile sig_atomic_t from_self;

/* With SA_SIGINFO the handler takes three arguments — who sent it, and why */
static void on_signal(int sig, siginfo_t *info, void *ctx)
{
    (void)sig; (void)ctx;
    hits++;
    last_code = info->si_code;
    from_self = (info->si_pid == getpid());   /* the value itself is not printed */
}

static const char *code_name(int code)
{
    switch (code) {
    case SI_USER:   return "SI_USER (sent by kill/raise)";
    case SI_QUEUE:  return "SI_QUEUE (sigqueue)";
    case SI_TIMER:  return "SI_TIMER (a timer)";
    case SI_KERNEL: return "SI_KERNEL (sent by the kernel)";
    case -6:        return "SI_TKILL (raised by the same thread, Linux)";
    default:        return "some other implementation-defined value";
    }
}

int main(void)
{
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);          /* start the struct wholly zeroed */
    sa.sa_sigaction = on_signal;        /* with SA_SIGINFO, fill this one */
    sigemptyset(&sa.sa_mask);           /* extra signals blocked while handling */
    sigaddset(&sa.sa_mask, SIGUSR2);    /* defer USR2 while handling USR1 */
    sa.sa_flags = SA_SIGINFO | SA_RESTART;  /* extra info + restart syscalls */

    struct sigaction old;
    if (sigaction(SIGUSR1, &sa, &old) != 0) return 1;

    printf("SIGUSR1 installed with sigaction. Previous handler: %s\n",
           old.sa_handler == SIG_DFL ? "SIG_DFL" : "present");

    /* raise it three times — unlike signal(), the handler *stays* */
    for (int i = 0; i < 3; i++) (void)raise(SIGUSR1);
    printf("after three raises, handled = %d (the handler persists)\n", (int)hits);
    printf("  was the sender ourselves? %s\n", from_self ? "yes" : "no");
    printf("  si_code = %d -> %s\n", (int)last_code, code_name((int)last_code));

    /* -- blocking a signal for a while ---------------------------------- */
    sigset_t block, prev;
    sigemptyset(&block);
    sigaddset(&block, SIGUSR1);
    if (sigprocmask(SIG_BLOCK, &block, &prev) != 0) return 1;

    int before = (int)hits;
    (void)raise(SIGUSR1);               /* blocked, so it becomes pending */
    printf("\nraised while blocked: handled %d -> %d (not delivered yet)\n",
           before, (int)hits);

    sigset_t pending;
    sigpending(&pending);
    printf("  is it pending? %s\n",
           sigismember(&pending, SIGUSR1) ? "yes" : "no");

    sigprocmask(SIG_SETMASK, &prev, nullptr);   /* unblock: delivered right here */
    printf("  handled just after unblocking = %d\n", (int)hits);

    /* -- a look inside the structures ----------------------------------- */
    printf("\nsizeof(struct sigaction) = %zu bytes, sigset_t = %zu bytes\n",
           sizeof(struct sigaction), sizeof(sigset_t));
    printf("SA_RESTART=0x%x, SA_SIGINFO=0x%x, SA_NOCLDWAIT=0x%x\n",
           (unsigned)SA_RESTART, (unsigned)SA_SIGINFO, (unsigned)SA_NOCLDWAIT);
    return 0;
}

Output

SIGUSR1 installed with sigaction. Previous handler: SIG_DFL
after three raises, handled = 3 (the handler persists)
  was the sender ourselves? yes
  si_code = -6 -> SI_TKILL (raised by the same thread, Linux)

raised while blocked: handled 3 -> 3 (not delivered yet)
  is it pending? yes
  handled just after unblocking = 4

sizeof(struct sigaction) = 152 bytes, sigset_t = 128 bytes
SA_RESTART=0x10000000, SA_SIGINFO=0x4, SA_NOCLDWAIT=0x2

76.7.1 Inside struct sigaction

This structure is this chapter’s data-type story. POSIX fixes four members and does not fix their order (so it must be started with a designated initializer or memset).

MemberTypeWhat it is
sa_handlervoid (*)(int)The plain handler, shaped like the standard’s
sa_sigactionvoid (*)(int, siginfo_t *, void *)The one used with SA_SIGINFO; more information arrives
sa_masksigset_tSignals blocked while this handler runs
sa_flagsintFlags choosing the behaviour (below)

Table 77.9

sa_handler and sa_sigaction usually overlap in a union (chapter 47). So only one is filled, and the SA_SIGINFO flag says which.

The four flags most often seen:

FlagMeaning
SA_SIGINFOUse the three-argument handler — who sent it, and why
SA_RESTARTAutomatically restart system calls interrupted by the signal
SA_NOCLDWAITLeave no zombie when a child ends (SIGCHLD)
SA_RESETHANDReset to the default after one delivery, the old way

Table 77.10

76.7.2 siginfo_t — who sent it, and why

With SA_SIGINFO the handler receives a siginfo_t *. The members most used:

MemberWhat it is
si_signoThe signal number
si_codeWhy it came — SI_USER (kill), SI_KERNEL, SI_TIMER, …
si_pidThe sending process’s id
si_uidThe sending user’s id
si_addrFor SIGSEGV and SIGBUS, the address that faulted

Table 77.11

The demonstration prints si_code. Raised at ourselves, Linux put SI_TKILL (−6) there — “sent by the same thread”. The names and meanings of these values differ per implementation, so check that platform’s documentation before branching on one.

si_addr earns its keep in debugging. Record it in a SIGSEGV handler and you learn “which address it died touching” — though it cannot be printed from inside that handler (the permitted list), so raw bytes are usually written with write and the program ended with _Exit.

76.7.3 Blocking a signal for a while — the mask

sigprocmask declares “I will not receive this signal for now”. A signal arriving while blocked does not vanish; it stays pending and is delivered the moment the block is lifted. The demonstration’s last block is that scene — raised while blocked, the count stayed put; unblocked, it rose at once.

Where this is needed is clear. When a signal must not cut in while a data structure is being fixed, block it for that stretch. It makes a critical section against signals as well.

76.8 Real uses

Now to what signals actually do in practice.

SignalUseRepresentative case
SIGTERMA graceful shutdown request — time to clean upkill’s default, container shutdown in Docker and Kubernetes
SIGINTThe user’s interruption (Ctrl+C)A command-line tool wrapping up its work
SIGKILLKill at once — cannot be caughtThe last resort when graceful shutdown fails
SIGHUPRe-read configuration (by convention)nginx and Apache reloading without downtime
SIGCHLDA child has endedShells and servers reaping zombies
SIGPIPEWrote to a pipe whose reader is goneServers mostly ignore it and handle EPIPE
SIGWINCHThe terminal was resizedvim and top redrawing the screen
SIGUSR1, SIGUSR2The application decides the meaningnginx’s live binary upgrade, reopening log files

Table 77.12

76.8.1 Graceful shutdown — the most widely used pattern

It became especially important in the container world. An orchestrator taking a container down sends SIGTERM first, and kills it with SIGKILL if it has not finished within the grace period (usually 30 seconds). So a server receiving SIGTERM must stop accepting new requests, finish those in flight, close its connections and go down.

The flag pattern above does exactly this work. The handler only sets stop = 1 and the main loop sees it and walks through the cleanup.

76.8.2 SIGPIPE — the signal whose right answer is to ignore it

Write to a pipe or socket whose reader has already gone and SIGPIPE arrives. The default action is terminating the process — for a web server, dying because a client closed a window.

So network programs almost invariably begin like this:

signal(SIGPIPE, SIG_IGN);   /* let write return -1/EPIPE instead of a signal */

Ignored, write returns the failure as a value (errno == EPIPE). Chapter 75′s “failure as a value” is the better arrangement here too.

76.8.3 Interrupted system calls — EINTR

When a signal arrives, slow system calls such as read and write are cut off, return −1 and put EINTR in errno. Not knowing this produces the ghost bug of “reads sometimes fail”.

There are two prescriptions: give SA_RESTART so the kernel restarts them, or retry by hand.

ssize_t n;
do { n = read(fd, buf, len); } while (n < 0 && errno == EINTR);

SA_RESTART is not a cure-all either — some calls are not restarted (notably those with timeouts), so robust code keeps the retry loop as well.

In practice. The self-pipe trick and its descendants

Mixing signals with an event loop was a long-standing nuisance. A signal arriving while waiting in select or poll breaks the loop, and almost nothing may be done inside the handler.

So in the 1990s the self-pipe trick appeared. A program makes a pipe to itself, and the handler writes a single byte into it (write is on the safe list). The event loop then receives that as an ordinary readable event — the signal has been turned into a file descriptor.

Today Linux offers signalfd, providing the idea in the kernel directly, and the BSDs have kqueue’s EVFILT_SIGNAL. Different names, same idea — turn a signal from an asynchronous interruption into an event that queues.

Q. In a program with several threads, where does a signal go?

A. A delicate place, and without knowing the rules it becomes a bug that is hard to reproduce.

A signal sent to the process (kill) is delivered to any one thread that has not blocked it — which one is not fixed. pthread_kill, by contrast, goes to the thread named. And while handlers are shared by the whole process, the mask is per thread.

So the standard practice is this — block the signal in every thread and let one dedicated thread wait for it with sigwait. The signal then turns from an asynchronous interruption into an ordinary function return, and inside that thread printf and malloc are free to use. It is the same idea as signalfd above.

76.8.4 Which software uses signals, and why

The reason for using signals differs from program to program, and those reasons make the device’s place clear.

WhatWhat it uses them forWhy it had to be a signal
nginx, ApacheSIGHUP to reload configuration, SIGUSR2 to swap the executableThe cheapest channel by which an operator gets from outside to inside. No port, no socket
PostgreSQLQuery cancellation and shutdown requests (the handler only raises a flag)One process per connection, so a signal between processes is the means of communication
RedisSIGSEGV/SIGBUS handlers that print a crash reportThe last chance to record the state at the moment of death — into a pre-allocated buffer, out through write
The HotSpot JVMSIGSEGV for null checks and safepointsRemoves the check from the normal path entirely and leaves the rare case to a hardware trap
WebAssembly runtimesSIGSEGV/SIGBUS trap handlers for out-of-bounds accessLeaves bounds checking to guard pages, removing a compare from every access
The Boehm GCmprotect + SIGSEGV as a write barrier, signals to stop threadsThe only way to put a collector on a C program without help from the language
libuv, Node.jsA dedicated thread and a pipe turn signals into eventsTo mix with an event loop, a signal must become a file descriptor
CPythonThe handler only raises a flag; the bytecode loop checks itInterpreter state cannot be touched from a handler — the permitted list again
libcurlIgnores SIGPIPE; older versions used SIGALRM to time out name resolutionA legacy of days with no other way to impose a timeout. Risky enough to deserve its own off switch (CURLOPT_NOSIGNAL)

Table 77.13

They sort into three groups. Instructions arriving from outside (nginx, PostgreSQL — the channel of operation), lifting a hardware trap into user code (the JVM, WebAssembly, the GC, Redis — emptying the normal path and signalling only the exception), and turning signals into events (libuv, CPython — the modern prescription for getting around the permitted list).

The middle group is the interesting one. “Remove the null check and catch it with SIGSEGV” is the archetype of an optimisation that pushes the cost onto the rare case — one instruction is deleted from the normal path at the price of a long trip through the kernel and a handler when the accident happens. It is the same calculation as chapter 11′s “a rare branch may be expensive.”

Counter-example. cleaning up directly in the handler

static void on_term(int sig) {
    (void)sig;
    fclose(logfile);        /* standard library — forbidden */
    free(buffer);           /* forbidden */
    printf("bye\\n");        /* forbidden */
    exit(0);                /* exit is forbidden too (_Exit is allowed) */
}

All four are outside the permitted list. And this code is more dangerous for mostly working — the trouble only surfaces under load or when signals crowd in.

The correct form is one flag.

static volatile sig_atomic_t stop;
static void on_term(int sig) { (void)sig; stop = 1; }

Recap

What to keepThe point
What it isNeither a thread nor a call — a third thing that cuts in unpredictably
The standard’s scopeTwo functions (signal, raise) and six signals
signal’s returnThe previous disposition; SIG_ERR means failure
Inside a handlerThe permitted list is all there is — in practice, one assignment
volatile sig_atomic_tBoth are needed: one against half values, one against optimisation
Allocationmalloc is barred by the free list and the lock — obtain vessels in advance
ContextThe kernel restores every register. Only errno must be saved by hand
POSIXsigaction fills the three gaps (reset, re-entry, EINTR)
In practiceHandler raises a flag, the main flow cleans up; ignore SIGPIPE
Modern alternativessignalfd, kqueue, a dedicated thread — turn signals into events

Table 77.14

We have handled the interruption that arrives from outside. The next chapter is its opposite — the device with which a program cuts its own flow and leaps back up the stack, setjmp and longjmp.