Code of the Day
AdvancedSystems programming patterns

Signals and signal handlers

Handle Unix signals in C — installing signal handlers, the restrictions on async-signal-safe code, and using sigaction for reliable signal handling.

CAdvanced11 min read
By the end of this lesson you will be able to:
  • Explain what a signal is and when the OS sends one
  • Install a signal handler using sigaction
  • Apply the async-signal-safe rules to handler code
  • Use a volatile sig_atomic_t flag to communicate from a signal handler to the main loop

A signal is an asynchronous notification delivered by the OS to a process. When you press Ctrl-C in a terminal, the OS sends SIGINT to the foreground process. When a program accesses an invalid memory address, the OS sends SIGSEGV. When a child process exits, the parent receives SIGCHLD. Understanding signals is essential for writing robust Unix tools and daemons.

Common signals

SignalDefault actionCommon cause
SIGINT (2)TerminateCtrl-C
SIGTERM (15)Terminatekill pid
SIGKILL (9)Terminate (uncatchable)kill -9 pid
SIGSEGV (11)Core dumpNull/invalid pointer
SIGCHLD (17)IgnoreChild process exited
SIGHUP (1)TerminateTerminal closed; reload config
SIGPIPE (13)TerminateWrite to closed pipe
SIGALRM (14)TerminateTimer expired (from alarm())

sigaction — the reliable way to install a handler

The older signal() function has platform-specific behaviour. sigaction() is the POSIX-standard, reliable way:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t shutdown_requested = 0;

static void handle_sigint(int signum) {
    (void)signum;
    shutdown_requested = 1; /* set a flag; do nothing else */
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask); /* do not block additional signals during handler */
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    printf("Press Ctrl-C to exit\n");
    while (!shutdown_requested) {
        printf("Running...\n");
        sleep(1);
    }
    printf("Shutting down cleanly\n");
    return 0;
}

The async-signal-safe requirement

Signal handlers can interrupt any code at any moment — including code inside malloc, printf, or your own mutex operations. These functions are not async-signal-safe — calling them from a signal handler can deadlock or corrupt data.

The only safe operations inside a signal handler:

  • Set a volatile sig_atomic_t flag.
  • Call a small set of explicitly async-signal-safe functions (write, _exit, sem_post, sigaction).
  • Perform async-signal-safe arithmetic (integer operations on sig_atomic_t).

The printf and fprintf functions are not safe to call from a signal handler. Use write(STDOUT_FILENO, msg, len) for output inside a handler.

static void safe_handler(int sig) {
    (void)sig;
    const char msg[] = "Signal received\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1); /* async-signal-safe */
    shutdown_requested = 1;
}

Self-pipe trick for integrating signals with event loops

When a program uses select/poll/epoll for I/O, signals interrupt the call and cause it to return EINTR. The self-pipe trick converts the signal into a readable file descriptor event:

int pipefd[2];
pipe(pipefd); /* create a pipe */

/* Signal handler: write one byte to the write end */
static void handler(int sig) {
    (void)sig;
    char byte = 0;
    write(pipefd[1], &byte, 1);
}

/* Event loop: add pipefd[0] to your select/poll set */
/* When pipefd[0] is readable, a signal arrived */

This makes signals composable with I/O in event-driven programs.

SIGPIPE and network programming

When you write to a socket after the remote end has closed, the OS sends SIGPIPE. The default action is to terminate the process — which is usually not what you want. The common fix:

/* Ignore SIGPIPE -- write() will return EPIPE error instead */
struct sigaction sa = {0};
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, NULL);

Then check for EPIPE in the write return value.

Signals and threads. In a multithreaded program, signals are delivered to one thread (either a specific thread or an arbitrary one). Blocking signals in worker threads and handling them only in a dedicated signal-handling thread (using sigwait) is the cleanest approach for multithreaded servers.

Where to go next

Next: fork and exec — creating child processes to run other programs, and the fork/exec/wait lifecycle.

Finished reading? Mark it complete to track your progress.

On this page