CFoundations

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

TT
TopicTrick Team
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?

mermaid

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

SignalDefaultWhen Sent
SIGINT (2)TerminateUser presses Ctrl+C
SIGTERM (15)Terminatekill pid — request for graceful shutdown
SIGQUIT (3)Core dumpUser presses Ctrl+\
SIGKILL (9)Terminatekill -9cannot be caught or ignored
SIGHUP (1)TerminateTerminal closed; often used for config reload
SIGSEGV (11)Core dumpSegmentation fault (invalid memory access)
SIGBUS (7)Core dumpBus error (misaligned access)
SIGFPE (8)Core dumpFloating-point exception (division by zero)
SIGPIPE (13)TerminateWrite to broken pipe (read end closed)
SIGCHLD (17)IgnoreChild process terminated
SIGALRM (14)TerminateTimer from alarm() expired
SIGUSR1/2TerminateUser-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():

c

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:

c

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:

text

Notable UNSAFE functions (never call from a handler):

  • printf, fprintf, sprintf — use internal locks and malloc
  • malloc, free, realloc — use internal heap locks
  • exit() — 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

c

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:

c

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

c

[!CAUTION] When SIGSEGV fires, your program's memory may already be corrupted. The only truly safe actions are write(), _exit(), and re-raising with signal(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:

c

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.