Linux System Programming · advanced · ~12 min
Handle SIGINT and SIGTERM gracefully so your program can shut down cleanly instead of being killed mid-task.
A signal is an asynchronous notification the kernel delivers to a process. "Asynchronous" means it can arrive at any moment, interrupting whatever your code is currently doing.
Signals tell a process that something important happened. For example:
You respond to a signal by installing a handler — a small function the kernel calls when the signal arrives. You install handlers with sigaction (recommended) or the older signal function.
Signals are how the kernel and other processes get your attention urgently.
You already meet them constantly:
kill command sends a signal.Learning to handle signals lets your program decide what happens at these moments, instead of just being terminated.
Disposition. Every signal has a default action, called its disposition: ignore it, terminate the process, or terminate and dump core (write a crash file for debugging). You can install your own handler to override the default.
Signal-safe functions. A handler can interrupt your code at any point, so it may only call functions that are async-signal-safe — safe to run in that interrupted state. Only a small list qualifies (see signal-safety(7)):
write, _exit, reading a sig_atomic_t.printf, malloc, and anything from stdio.Async-signal-safe communication. The standard pattern is to do almost nothing in the handler. Set a flag:
volatile sig_atomic_t flag = 1;
Then let your main loop notice the flag and do the real work. sig_atomic_t is an integer type guaranteed to be read and written in one uninterruptible step. volatile tells the compiler the value can change at any time, so it must re-read it each loop.
Install a handler for SIGINT and exit the loop when it fires:
#include <signal.h>
volatile sig_atomic_t stop = 0;
static void on_sigint(int sig) { (void)sig; stop = 1; }
int main(void) {
struct sigaction sa = {.sa_handler = on_sigint};
sigaction(SIGINT, &sa, NULL);
while (!stop) { /* work */ }
}
A signal is an asynchronous notification — it can arrive at any time. For example, Ctrl-C sends SIGINT, and kill <pid> sends SIGTERM.
Install a handler with either function:
signal() — simple, but has historically unreliable behavior across systems.sigaction() — modern and recommended.Keep handlers tiny. Inside a handler you may only call async-signal-safe functions, and you should only touch volatile sig_atomic_t variables.
The safe pattern is two steps:
static volatile sig_atomic_t g_stop = 0;
static void on_term(int s) { (void)s; g_stop = 1; }
/* ... */
signal(SIGTERM, on_term);
while (!g_stop) { /* work */ }
printf from a signal handler. It is not async-signal-safe and can corrupt program state. Set a flag instead, and print from the main loop.kill -l lists every signal name and number.gdb, handle SIGPIPE nostop noprint lets you keep debugging through signals you expect, instead of stopping each time.sigaction and not the old signal(). signal() has historically unreliable semantics.Calling non-async-signal-safe functions from a handler can corrupt the heap or stdio buffers, because the handler may have interrupted those very functions mid-operation.
The safe pattern avoids this entirely: just set a flag in the handler, then react in the main loop.
Note that SIGKILL and SIGSTOP cannot be caught or handled. That is by design — it guarantees a process can always be stopped or killed.
Signals drive many everyday behaviors:
sigaction with the SA_RESTART flag so a blocking read survives an interruption (an EINTR error) instead of failing.sigaction (preferred over the older signal).volatile sig_atomic_t flag or write a byte.