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():
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:
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:
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
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:
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
[!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:
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.
