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:

  1. 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 via signal(), the process was abruptly killed.
  2. Interrupted Syscalls: Operating systems differed on whether blocking system calls (such as read() or accept()) should automatically resume or fail with EINTR.

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 as read(), write(), or waitpid()) if interrupted by this signal, preventing transient -1 returns with errno == EINTR.
  • SA_SIGINFO: Invokes sa_sigaction instead of sa_handler. Provides rich event metadata via siginfo_t (e.g. sending process PID, memory address of faulting instruction si_addr, user data payload).
  • SA_NOCLDWAIT: Applied to SIGCHLD to 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] SIGKILL and SIGSTOP cannot 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

  1. Flag Pattern: Set a volatile sig_atomic_t or atomic_bool flag 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;
    }
    
  2. Self-Pipe Trick: Write 1 byte to a non-blocking pipe or eventfd monitored by an epoll() or poll() 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
    }
    
  3. 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 -9 fails on D state 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