Linux System Programming · advanced · ~12 min

Signals

Handle SIGINT and SIGTERM gracefully so your program can shut down cleanly instead of being killed mid-task.

Overview

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:

  • SIGINT — someone pressed Ctrl-C.
  • SIGCHLD — a child process exited.
  • SIGPIPE — you tried to write to a closed pipe.
  • SIGTERM / SIGKILL — something wants to terminate the process.

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.

Why it matters

Signals are how the kernel and other processes get your attention urgently.

You already meet them constantly:

  • Every Ctrl-C is a signal (SIGINT).
  • Every kill command sends a signal.
  • Every segfault you've ever seen was a signal (SIGSEGV) reaching its default handler.

Learning to handle signals lets your program decide what happens at these moments, instead of just being terminated.

Core concepts

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)):

  • Safe: write, _exit, reading a sig_atomic_t.
  • Forbidden: 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.

Syntax notes

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 */ }
}

Lesson

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:

  1. Set a flag in the handler.
  2. Do the actual work back in the main loop.

Code examples

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 */ }

Common mistakes

  • Calling 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.

Debugging tips

  • kill -l lists every signal name and number.
  • In gdb, handle SIGPIPE nostop noprint lets you keep debugging through signals you expect, instead of stopping each time.
  • If your handler seems to do nothing, check that you used sigaction and not the old signal(). signal() has historically unreliable semantics.

Memory safety

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.

Real-world uses

Signals drive many everyday behaviors:

  • Graceful shutdown of servers (SIGTERM).
  • Reloading configuration without a restart (SIGHUP).
  • Child cleanup when a subprocess exits (SIGCHLD).
  • Broken-pipe handling when writing to a closed socket (SIGPIPE).
  • Aborting a running program with Ctrl-C (SIGINT).

Practice tasks

  1. Install a SIGINT handler that prints a message and exits cleanly.
  2. Run an infinite loop, then press Ctrl-C and confirm your handler runs.
  3. Use sigaction with the SA_RESTART flag so a blocking read survives an interruption (an EINTR error) instead of failing.

Summary

  • A signal is an asynchronous notification from the kernel; it can arrive at any time.
  • Install handlers with sigaction (preferred over the older signal).
  • In a handler, do only async-signal-safe work — typically set a volatile sig_atomic_t flag or write a byte.
  • Let the main loop react to the flag. This pattern handles Ctrl-C, child exits, and shutdowns gracefully.

Practice with these exercises