When a user presses Ctrl+C, the kernel sends SIGINT to your process. By default, the program terminates immediately — no cleanup, no flush, no graceful shutdown. Signal handlers let you intercept signals and decide what to do. Used correctly, they make your programs robust. Used incorrectly, they introduce race conditions that are nearly impossible to debug.

Registering a Signal Handler

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

volatile sig_atomic_t running = 1;

void handle_sigint(int signum) {
    running = 0;   /* set flag, return — do NOT do complex work here */
}

int main() {
    signal(SIGINT, handle_sigint);   /* Ctrl+C */

    printf("Running. Press Ctrl+C to stop.n");

    while (running) {
        printf("working...n");
        sleep(1);
    }

    printf("nShutdown complete.n");
    return 0;
}

The pattern: the signal handler sets a flag. The main loop checks the flag and exits cleanly. This is the correct approach because signal handlers run in an interrupt context — most C library functions are unsafe to call from them.

sigaction — The Better Alternative to signal()

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

volatile sig_atomic_t stop = 0;

void handler(int signum) {
    stop = 1;
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;   /* auto-restart interrupted syscalls */

    sigaction(SIGINT,  &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    while (!stop) {
        /* do work */
        sleep(1);
    }

    printf("Caught signal — shutting downn");
    return 0;
}

sigaction is preferred over signal() because it provides:

  • SA_RESTART — automatically restarts system calls interrupted by the signal (like read)
  • sa_mask — blocks specified signals while the handler runs
  • Consistent behavior across platforms (signal() semantics vary)

Common Signals

SignalDefault actionWhen sent
SIGINTTerminateCtrl+C from terminal
SIGTERMTerminatekill <pid> — polite shutdown request
SIGKILLTerminatekill -9 <pid> — cannot be caught
SIGHUPTerminateTerminal closed, or daemon reload convention
SIGALRMTerminatealarm() timer expired
SIGSEGVCore dumpInvalid memory access
SIGCHLDIgnoredChild process exited

Async-Signal Safety — The Critical Constraint

/* UNSAFE in a signal handler: */
void bad_handler(int sig) {
    printf("Caught %dn", sig);   /* malloc inside printf — not async-signal-safe */
    free(ptr);                     /* heap operations are not async-signal-safe */
    exit(0);                       /* technically safe, but _exit is preferred */
}

/* SAFE in a signal handler: */
void good_handler(int sig) {
    static const char msg[] = "Caught signaln";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);  /* write() is async-signal-safe */
    _exit(0);   /* _exit skips atexit handlers and buffer flushing */
}

Signal handlers can interrupt code at any point — including inside malloc or printf. If your handler calls malloc while the main code was also inside malloc, you corrupt the heap. POSIX defines a list of async-signal-safe functions (around 70 functions including write, read, _exit, and the signal functions themselves). Only call those from a signal handler, or set a flag and return.

Implementing a Cleanup Handler

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>

static FILE *log_file = NULL;
volatile sig_atomic_t shutdown_requested = 0;

void cleanup(void) {
    if (log_file) {
        fflush(log_file);
        fclose(log_file);
        log_file = NULL;
    }
}

void handle_term(int signum) {
    shutdown_requested = 1;
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_term;
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT,  &sa, NULL);

    atexit(cleanup);   /* register cleanup for normal exit too */

    log_file = fopen("app.log", "a");
    if (!log_file) { perror("fopen"); return 1; }

    while (!shutdown_requested) {
        fprintf(log_file, "tickn");
        sleep(1);
    }

    /* cleanup() called automatically via atexit */
    return 0;
}

Register cleanup logic with atexit — it runs both on normal return from main and when exit() is called. The signal handler sets the flag; the main loop exits normally; atexit handles cleanup. This keeps complex logic out of the signal handler entirely. To compile and test the signal-handling setup, use our c program compiler. For a deeper look at the build flags used with POSIX code, see the guide on GCC flags and standards.

TL;DR

  • Signal handlers run at any point — assume they interrupt any code in your program
  • Only call async-signal-safe functions in handlers (about 70 POSIX functions)
  • The correct pattern: set a volatile sig_atomic_t flag in the handler; check it in the main loop
  • Prefer sigaction over signal() — use SA_RESTART to avoid broken system calls
  • SIGKILL cannot be caught or ignored — always assume processes may be killed hard
  • Use atexit() to register cleanup functions that run on both normal exit and signal-triggered exit
  • Do not call printf, malloc, or free from signal handlers