C Signals & Interrupt Handling: Graceful Shutdown, Crash Reporting & Async Events

C Signals & Interrupt Handling: Graceful Shutdown, Crash Reporting & Async Events
Table of Contents
- What Are Signals?
- Important POSIX Signal Reference
- signal() vs sigaction(): The Right Tool
- Writing Correct Signal Handlers
- Signal-Safe Functions: The Strict List
- volatile sig_atomic_t: The Communication Bridge
- Graceful Server Shutdown Pattern
- Crash Handler for SIGSEGV
- Blocking Signals with sigprocmask
- Real-Time Signals: sigqueue and SA_SIGINFO
- Frequently Asked Questions
- Key Takeaway
What Are Signals?
Signals are delivered asynchronously — they can arrive while your code is in the middle of a strlen(), a malloc(), or even inside another signal handler. This asynchronous nature is what makes signal handlers so restricted: almost any function you call from a handler could be corrupted or re-entered.
Important POSIX Signal Reference
| Signal | Default | When Sent |
|---|---|---|
SIGINT (2) | Terminate | User presses Ctrl+C |
SIGTERM (15) | Terminate | kill pid — request for graceful shutdown |
SIGQUIT (3) | Core dump | User presses Ctrl+\ |
SIGKILL (9) | Terminate | kill -9 — cannot be caught or ignored |
SIGHUP (1) | Terminate | Terminal closed; often used for config reload |
SIGSEGV (11) | Core dump | Segmentation fault (invalid memory access) |
SIGBUS (7) | Core dump | Bus error (misaligned access) |
SIGFPE (8) | Core dump | Floating-point exception (division by zero) |
SIGPIPE (13) | Terminate | Write to broken pipe (read end closed) |
SIGCHLD (17) | Ignore | Child process terminated |
SIGALRM (14) | Terminate | Timer from alarm() expired |
SIGUSR1/2 | Terminate | User-defined; typically used for app-specific events |
signal() vs sigaction(): The Right Tool
The old signal() function is portable but has several implementation-defined behaviors. The modern replacement is sigaction():
#include <signal.h>
#include <stdio.h>
void old_handler(int sig) {
// signal() may or may not auto-reset the handler on delivery
// behavior on concurrent signals is implementation-defined
printf("SIGINT via signal()\n");
}
// sigaction: precise, portable, thread-safe
void new_handler(int sig) {
// Handler body
}
int setup_signals(void) {
struct sigaction sa;
sa.sa_handler = new_handler;
sigemptyset(&sa.sa_mask); // No additional signals blocked during handler
sa.sa_flags = SA_RESTART; // Restart syscalls interrupted by signal
if (sigaction(SIGINT, &sa, NULL) < 0) return -1;
if (sigaction(SIGTERM, &sa, NULL) < 0) return -1;
// Ignore SIGPIPE globally (common in server code)
sa.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &sa, NULL) < 0) return -1;
return 0;
}SA_RESTART: Without this flag, if a signal interrupts a blocking syscall (like read(), accept(), sleep()), the syscall returns with errno = EINTR. With SA_RESTART, the OS automatically restarts those syscalls after the handler returns.
sa.sa_mask: Signals to block during handler execution. sigemptyset blocks none. sigfillset blocks all signals while the handler runs (prevents re-entrant handling).
Writing Correct Signal Handlers
Signal handlers run asynchronously on your thread's current stack, potentially interrupting any function mid-execution. Most standard library functions are NOT safe to call from a handler:
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// BAD handler — calls async-signal-UNSAFE functions
void bad_handler(int sig) {
printf("Received signal %d\n", sig); // UNSAFE: printf uses malloc internally
free(some_ptr); // UNSAFE: malloc/free are not re-entrant
fclose(some_file); // UNSAFE: stdio is not re-entrant
}
// GOOD handler — minimalist, signal-safe
volatile sig_atomic_t g_shutdown_requested = 0;
void good_handler(int sig) {
// ONLY: set a volatile flag
g_shutdown_requested = 1;
// Async-signal-safe functions we can call:
// write() — direct syscall, no buffering
const char msg[] = "Signal received\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
}Signal-Safe Functions: The Strict List
POSIX defines the complete list of async-signal-safe functions — the only ones you may call from a signal handler. Key safe functions include:
write() read() open() close() _exit()
kill() getpid() getppid() alarm() pause()
raise() signal() sigaction() sigemptyset() sigfillset()
waitpid() fork() execve()Notable UNSAFE functions (never call from a handler):
printf,fprintf,sprintf— use internal locks and mallocmalloc,free,realloc— use internal heap locksexit()— calls atexit handlers (non-reentrant)syslog()— may use locks- Any
pthread_*function (except a small subset)
The safe pattern: use the handler to set a volatile sig_atomic_t flag, then check the flag in your main loop.
volatile sig_atomic_t: The Communication Bridge
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
volatile sig_atomic_t g_running = 1; // Main loop control
volatile sig_atomic_t g_reload_config = 0; // Config reload request
void sigterm_handler(int sig) {
g_running = 0; // Signal the main loop to stop
}
void sighup_handler(int sig) {
g_reload_config = 1; // Signal config reload
}
int main(void) {
struct sigaction sa_term = { .sa_handler = sigterm_handler,
.sa_flags = SA_RESTART };
sigemptyset(&sa_term.sa_mask);
sigaction(SIGTERM, &sa_term, NULL);
sigaction(SIGINT, &sa_term, NULL);
struct sigaction sa_hup = { .sa_handler = sighup_handler,
.sa_flags = SA_RESTART };
sigemptyset(&sa_hup.sa_mask);
sigaction(SIGHUP, &sa_hup, NULL);
printf("Server running (PID %d). Send SIGTERM or Ctrl+C to stop.\n", getpid());
while (g_running) {
if (g_reload_config) {
g_reload_config = 0;
printf("Reloading configuration...\n");
// do_reload_config();
}
// Normal server work
sleep(1);
}
// Main loop exited cleanly — perform graceful shutdown
printf("Shutting down gracefully...\n");
// cleanup_connections();
// flush_write_buffers();
return 0;
}Why volatile sig_atomic_t?
volatile: Prevents the compiler from caching the variable in a register — the main loop reads it from memory on every iteration.sig_atomic_t: Guaranteed to be read and written atomically on all platforms — no partial reads where a signal could interrupt between loading the high byte and low byte of the value.
Graceful Server Shutdown Pattern
Production servers must clean up on SIGTERM — close client connections, flush write buffers, save state:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static volatile sig_atomic_t g_stop = 0;
static void *g_cleanup_ctx = NULL;
void signal_handler(int sig) {
g_stop = 1;
}
void setup_graceful_shutdown(void) {
struct sigaction sa = {
.sa_handler = signal_handler,
.sa_flags = 0, // Don't restart syscalls — let accept() return EINTR
};
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
}
int run_server(int server_fd) {
setup_graceful_shutdown();
while (!g_stop) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) {
if (g_stop) break; // accept() interrupted by our signal — expected
perror("accept");
continue;
}
handle_client(client_fd);
close(client_fd);
}
// Here: g_stop == 1, clean up resources
printf("Graceful shutdown: closing %d active connections\n", active_connections);
// close_all_connections();
// write_shutdown_log();
return 0;
}Note: SA_RESTART is intentionally NOT set here — we want accept() to return EINTR when the signal arrives, allowing the while loop condition (!g_stop) to be checked.
Crash Handler for SIGSEGV
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void crash_handler(int sig) {
// Write crash info using only signal-safe write()
const char *msg;
if (sig == SIGSEGV) msg = "CRASH: Segmentation fault (SIGSEGV)\n";
else if (sig == SIGBUS) msg = "CRASH: Bus error (SIGBUS)\n";
else if (sig == SIGFPE) msg = "CRASH: Arithmetic error (SIGFPE)\n";
else msg = "CRASH: Unknown signal\n";
write(STDERR_FILENO, msg, strlen(msg)); // strlen is signal-safe
// Flash flush any critical logs (platform specific)
// fsync(log_fd); // If log_fd was opened before the crash
// Re-raise with default handler to generate core dump
signal(sig, SIG_DFL);
raise(sig);
}
void install_crash_handlers(void) {
struct sigaction sa = {
.sa_handler = crash_handler,
.sa_flags = SA_RESETHAND, // Reset to default after first invocation
};
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
}[!CAUTION] When
SIGSEGVfires, your program's memory may already be corrupted. The only truly safe actions arewrite(),_exit(), and re-raising withsignal(sig, SIG_DFL)+raise(sig). Don't attempt complex cleanup.
Blocking Signals with sigprocmask
Sometimes you need to protect a critical section from being interrupted by signals:
#include <signal.h>
void critical_section(void) {
sigset_t block_set, old_set;
// Define which signals to block
sigemptyset(&block_set);
sigaddset(&block_set, SIGTERM);
sigaddset(&block_set, SIGINT);
// Block them, save the old mask
sigprocmask(SIG_BLOCK, &block_set, &old_set);
// === Critical section — signals queued but not delivered ===
commit_transaction_to_disk();
// === End critical section ===
// Restore old mask — queued signals delivered now
sigprocmask(SIG_SETMASK, &old_set, NULL);
}This pattern ensures that a database transaction write is never interrupted mid-flight by a shutdown signal.
Frequently Asked Questions
Can I call exit() from a signal handler?
exit() is NOT async-signal-safe because it calls atexit() handlers and flushes stdio buffers. Use _exit() (no cleanup) or set a flag and let the main function call exit(). For crash handlers, _exit(1) after logging is acceptable.
Is SIGKILL really unblockable?
Yes — SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. They are implemented entirely in the kernel. When the OOM (Out-of-Memory) killer sends SIGKILL, your process has no opportunity to run any more code.
How do daemons handle SIGHUP for configuration reload?
The convention: send SIGHUP to request a config reload without restarting. The handler sets a g_reload_config flag. The main loop detects the flag, re-reads the config file, and updates internal state — all without losing active connections. nginx -s reload sends SIGHUP to the master process.
Are signal handlers thread-safe in multi-threaded programs?
Signal handlers are tricky in multi-threaded programs. A signal is delivered to one arbitrary thread (unless directed with pthread_kill). The recommended approach: use sigprocmask in all threads to block signals, then dedicate one thread to handle signals via sigwaitinfo or signalfd — this converts asynchronous signal delivery into synchronous I/O.
Key Takeaway
Signal handling is what makes C programs behave as Real Citizens of the Operating System rather than isolated scripts. By responding correctly to SIGTERM (clean shutdown), SIGHUP (reload config), and SIGSEGV (last-gasp crash report), you build production-quality software that interacts properly with system administrators, deployment tools (systemd, Docker), and monitoring systems.
Read next: C23 New Features: Modern C Evolution →
Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.
