POSIX Signals Architecture & sigaction
POSIX Signals are asynchronous software interrupts delivered by the operating system kernel to a process to notify it of hardware exceptions (such as unaligned memory or divide-by-zero) or system events (such as terminal resizing, timer expiration, or termination requests).
When a signal is delivered, the target process immediately suspends its regular execution flow, saves its register state, and executes a designated signal handler function before resuming its previous task.
Why signal(2) Is Deprecated
Early Unix introduced the signal(signum, handler) function, but its behavior was notorious for platform-dependent race conditions:
- Handler Reset Trap (System V): Upon delivering a signal, System V automatically reset the signal disposition back to
SIG_DFL(default). If a second signal arrived before the handler re-installed itself viasignal(), the process was abruptly killed. - Interrupted Syscalls: Operating systems differed on whether blocking system calls (such as
read()oraccept()) should automatically resume or fail withEINTR.
POSIX solved these flaws with sigaction(), providing deterministic signal masking, reliable execution, and precise control over system call restarts.
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
Configuring Handlers: struct sigaction
struct sigaction {
void (*sa_handler)(int); // Standard handler: void handler(int signum)
void (*sa_sigaction)(int, siginfo_t *, void *); // Advanced handler (requires SA_SIGINFO)
sigset_t sa_mask; // Additional signals to block during handler execution
int sa_flags; // Behavior modification flags
void (*sa_restorer)(void); // Obsolete/internal
};
Essential sa_flags
SA_RESTART: Automatically restarts slow, blocking system calls (such asread(),write(), orwaitpid()) if interrupted by this signal, preventing transient-1returns witherrno == EINTR.SA_SIGINFO: Invokessa_sigactioninstead ofsa_handler. Provides rich event metadata viasiginfo_t(e.g. sending process PID, memory address of faulting instructionsi_addr, user data payload).SA_NOCLDWAIT: Applied toSIGCHLDto prevent child processes from becoming zombie processes upon termination.SA_NODEFER: Does not automatically block the signal itself during its own handler execution (disabled by default to prevent recursive stack overflows).
The Signal Lifecycle
[ Event Occurs ] ──► 1. Generation
│
▼
2. Pending (Stored in kernel process pending set)
│
├─► (Blocked by sigprocmask? Remains pending until unblocked)
│
▼
3. Delivery (Kernel invokes handler or default action)
│
▼
4. Completion (sigreturn restores user-space stack & registers)
Signal Masking: sigprocmask()
A process can temporarily block specific signals from being delivered during critical sections using sigprocmask():
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
// Block SIGINT and SIGTERM
sigprocmask(SIG_BLOCK, &mask, &oldmask);
// --- Critical Section: Cannot be interrupted by SIGINT or SIGTERM ---
// Restore previous signal mask (delivers any pending signals)
sigprocmask(SIG_SETMASK, &oldmask, NULL);
[!NOTE]
SIGKILLandSIGSTOPcannot be caught, blocked, or ignored. They are processed unconditionally by the kernel.
Async-Signal Safety: The Strict Rules of Handlers
Because a signal can interrupt a thread at any arbitrary instruction—including inside malloc(), free(), or standard I/O library code—executing non-reentrant functions inside a signal handler causes deadlocks and memory corruption.
Prohibited Inside Signal Handlers
- ❌
malloc()/free()/new: Interrupting an ongoing heap allocation causes allocator lock deadlocks or heap corruption. - ❌
printf()/puts()/fprintf(): Standard I/O uses internal mutex locks for buffering. - ❌
pthread_mutex_lock(): Will deadlock if the interrupted thread already holds the mutex.
Allowed: Async-Signal-Safe Functions
POSIX guarantees a strict list of Async-Signal-Safe functions:
- File descriptor I/O:
write(),read(),close(). - Process termination:
_exit(),_Exit(). - Signal-safe primitives:
sem_post(),sigprocmask(),kill().
The Three Safe Handling Patterns
- Flag Pattern: Set a
volatile sig_atomic_toratomic_boolflag in the handler and inspect it in your main event loop:volatile sig_atomic_t g_shutdown_requested = 0; void handle_sigterm(int signum) { g_shutdown_requested = 1; } - Self-Pipe Trick: Write 1 byte to a non-blocking pipe or
eventfdmonitored by anepoll()orpoll()event loop:int pipe_fds[2]; void handle_signal(int signum) { char b = 'X'; write(pipe_fds[1], &b, 1); // write() is async-signal-safe } - Semaphore Wakeup: Call
sem_post(&sem)to awaken a worker thread.
Standard Signals vs. Real-Time Signals
| Feature | Standard POSIX Signals (1–31) |
Real-Time Signals (SIGRTMIN–SIGRTMAX) |
|---|---|---|
| Duplicates | Coalesced: Multiple identical signals merge into a single bit; lost if pending. | Queued: Every signal is reliably queued in the kernel. |
| Delivery Order | Undefined by standard; varies by implementation. | Strict FIFO for identical signals; lower numerical signals first. |
| Data Payload | None (signal number only). | Carries an integer or pointer payload via sigqueue(). |
Frequently Asked Questions (FAQ)
Why is printf() unsafe inside a signal handler?
Because printf() acquires internal stdio stream mutex locks and allocates dynamic buffer memory, risking instant deadlock or memory corruption if the signal interrupted another printf() or heap call. Instead, handlers should use the async-signal-safe write(STDERR_FILENO, ...) system call or set an atomic flag.
What does the SA_RESTART flag do in sigaction?
SA_RESTART instructs the operating system kernel to automatically restart slow, blocking system calls (like read() or accept()) interrupted by the signal instead of returning -1 with errno == EINTR. Without SA_RESTART, application code must manually enclose every blocking syscall in a retry loop checking for EINTR.
Can SIGKILL and SIGSTOP be caught or ignored?
No. POSIX guarantees that neither SIGKILL nor SIGSTOP can be caught, handled, blocked, or ignored by user space. When SIGKILL is issued, the kernel immediately and unconditionally destroys the process and reclaims its resources.
Key POSIX Signal Profiles
- SIGSEGV (Signal 11): Segmentation violation, NULL pointer dereferences, permission violations, and
sigaltstack. - SIGPIPE (Signal 13): Broken pipe on sockets, client disconnection handling, and
MSG_NOSIGNAL. - SIGHUP (Signal 1): Terminal disconnection,
nohup, and daemon hot-configuration reload. - SIGCHLD (Signal 17): Child process state changes, non-blocking reaping loops, and preventing zombie processes.
- SIGTERM (Signal 15): Graceful termination requests, Kubernetes pod lifecycles, and draining connections.
- SIGKILL (Signal 9): Uncatchable process destruction, OOM killer mechanics, and why
kill -9fails onDstate processes. - SIGINT (Signal 2): Terminal keyboard interrupt (
Ctrl+C), foreground process groups, and exit code 130. - SIGBUS (Signal 7): Memory bus faults and unaligned access.
- SIGSYS (Signal 31): Bad or restricted system call traps.
Related Topics
- POSIX Signals - SIGSEGV: Memory violation analysis.
- POSIX Signals - SIGBUS: Memory mapping bus faults and alignment errors.
- POSIX Signals - SIGSYS: Handling restricted or invalid system calls and seccomp traps.
- POSIX Process Groups & Sessions: Terminal job control,
SIGHUP, and signal propagation. - POSIX IPC Semaphores: Using
sem_post()for async-signal-safe event notification.
