76 Signals — <signal.h>
What to know first
errno and the handler’s restrictionsmain · a program’s start and endLooking 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
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
<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
- Why did the standard not take the better
sigaction? - Then what do you do when a handler really must record something?
- Does
setjmp/longjmpnot do the same thing? - 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.
| Declaration | What 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
| Place | What you give | Meaning |
|---|---|---|
sig | A signal number | One of the standard six, or a value the implementation defines |
func | SIG_DFL | Back to default handling (mostly termination) |
func | SIG_IGN | Ignore — the signal arrives and nothing happens |
func | A function pointer | Use that function as the handler |
| return | The previous disposition | SIG_DFL, SIG_IGN or the previous handler |
| return | SIG_ERR | Setting 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
| Name | When it arrives | Default action |
|---|---|---|
SIGABRT | A call to abort() | Abnormal termination |
SIGFPE | An arithmetic error (divide by zero, overflow, …) | Abnormal termination |
SIGILL | Executing an invalid instruction | Abnormal termination |
SIGINT | Interactive attention (usually Ctrl+C) | Termination |
SIGSEGV | An invalid memory access | Abnormal termination |
SIGTERM | A termination request | Termination |
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 permitted | Condition |
|---|---|
Assigning a value to a volatile sig_atomic_t object | Assigning, not reading |
| Handling lock-free atomic objects | <stdatomic.h>, when lock-free (chapter 80) |
Calling abort | — |
Calling _Exit | — |
Calling quick_exit | — |
Calling signal | Only 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.
| What | What it prevents | Without it |
|---|---|---|
sig_atomic_t | Seeing a half-written value | A partially updated value can be read |
volatile | The compiler eliding the read | The 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 in | If the handler calls malloc |
|---|---|
| The free-list links are half rewritten | It follows a half-rewritten list and hands out the wrong piece |
| Just before the block’s size is written | A later free returns it with the wrong size |
| While the allocator’s lock is held | It 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 signal | longjmp (chapter 77) | |
|---|---|---|
| Who saves | The kernel | The setjmp macro |
| What is saved | Every register | Only the callee-saved ones (eight slots) |
| Where it returns | The very instruction interrupted | The place that called setjmp |
| Local variables | All intact | No 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.
| Gap | Standard signal | sigaction |
|---|---|---|
| Does the handler survive? | Implementation-defined (it vanished in the demonstration) | It stays, unless SA_RESETHAND is given |
| If the same signal arrives while handling | Implementation-defined | Blocked by default; sa_mask blocks more |
| Interrupted system calls | Not settled | Restarted 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).
| Member | Type | What it is |
|---|---|---|
sa_handler | void (*)(int) | The plain handler, shaped like the standard’s |
sa_sigaction | void (*)(int, siginfo_t *, void *) | The one used with SA_SIGINFO; more information arrives |
sa_mask | sigset_t | Signals blocked while this handler runs |
sa_flags | int | Flags 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:
| Flag | Meaning |
|---|---|
SA_SIGINFO | Use the three-argument handler — who sent it, and why |
SA_RESTART | Automatically restart system calls interrupted by the signal |
SA_NOCLDWAIT | Leave no zombie when a child ends (SIGCHLD) |
SA_RESETHAND | Reset 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:
| Member | What it is |
|---|---|
si_signo | The signal number |
si_code | Why it came — SI_USER (kill), SI_KERNEL, SI_TIMER, … |
si_pid | The sending process’s id |
si_uid | The sending user’s id |
si_addr | For 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.
| Signal | Use | Representative case |
|---|---|---|
SIGTERM | A graceful shutdown request — time to clean up | kill’s default, container shutdown in Docker and Kubernetes |
SIGINT | The user’s interruption (Ctrl+C) | A command-line tool wrapping up its work |
SIGKILL | Kill at once — cannot be caught | The last resort when graceful shutdown fails |
SIGHUP | Re-read configuration (by convention) | nginx and Apache reloading without downtime |
SIGCHLD | A child has ended | Shells and servers reaping zombies |
SIGPIPE | Wrote to a pipe whose reader is gone | Servers mostly ignore it and handle EPIPE |
SIGWINCH | The terminal was resized | vim and top redrawing the screen |
SIGUSR1, SIGUSR2 | The application decides the meaning | nginx’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.
| What | What it uses them for | Why it had to be a signal |
|---|---|---|
| nginx, Apache | SIGHUP to reload configuration, SIGUSR2 to swap the executable | The cheapest channel by which an operator gets from outside to inside. No port, no socket |
| PostgreSQL | Query cancellation and shutdown requests (the handler only raises a flag) | One process per connection, so a signal between processes is the means of communication |
| Redis | SIGSEGV/SIGBUS handlers that print a crash report | The last chance to record the state at the moment of death — into a pre-allocated buffer, out through write |
| The HotSpot JVM | SIGSEGV for null checks and safepoints | Removes the check from the normal path entirely and leaves the rare case to a hardware trap |
| WebAssembly runtimes | SIGSEGV/SIGBUS trap handlers for out-of-bounds access | Leaves bounds checking to guard pages, removing a compare from every access |
| The Boehm GC | mprotect + SIGSEGV as a write barrier, signals to stop threads | The only way to put a collector on a C program without help from the language |
| libuv, Node.js | A dedicated thread and a pipe turn signals into events | To mix with an event loop, a signal must become a file descriptor |
| CPython | The handler only raises a flag; the bytecode loop checks it | Interpreter state cannot be touched from a handler — the permitted list again |
| libcurl | Ignores SIGPIPE; older versions used SIGALRM to time out name resolution | A 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 keep | The point |
|---|---|
| What it is | Neither a thread nor a call — a third thing that cuts in unpredictably |
| The standard’s scope | Two functions (signal, raise) and six signals |
signal’s return | The previous disposition; SIG_ERR means failure |
| Inside a handler | The permitted list is all there is — in practice, one assignment |
volatile sig_atomic_t | Both are needed: one against half values, one against optimisation |
| Allocation | malloc is barred by the free list and the lock — obtain vessels in advance |
| Context | The kernel restores every register. Only errno must be saved by hand |
| POSIX | sigaction fills the three gaps (reset, re-entry, EINTR) |
| In practice | Handler raises a flag, the main flow cleans up; ignore SIGPIPE |
| Modern alternatives | signalfd, 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.