sigaction and Signal Handlers

sigaction(2) is the modern, reliable way to examine or change what a process does when it receives a signal: ignore it, take the default action, or run a handler. It supersedes the old signal(2), whose semantics — does the handler stay installed? is the signal blocked while it runs? are syscalls restarted? — varied across Unix flavors and even across Linux versions, making it unsafe for anything beyond setting SIG_DFL/SIG_IGN (signal(2): “Avoid its use: use sigaction(2) instead.”). sigaction nails all of that down explicitly through a struct sigaction carrying the handler, a mask of additional signals to block during the handler, and a flag word; with SA_SIGINFO the handler also receives a rich siginfo_t describing why the signal fired. This note walks the structure field by field, the flags that change delivery semantics, and sigaltstack(2), the companion call that lets you catch SIGSEGV from a blown stack (sigaction(2)).

Mental Model — A Per-Signal Disposition Table

Think of every process (more precisely, every thread group sharing a sighand_struct) as owning a table with one slot per signal number. Each slot holds a disposition: SIG_DFL (do the kernel’s built-in default — terminate, dump core, stop, or ignore depending on the signal), SIG_IGN (discard it), or a pointer to a user function (run it). sigaction() is the read/modify/write operation on one slot. The flag word and mask attached to the slot are not afterthoughts — they decide whether the handler stays installed, what is blocked while it runs, whether interrupted syscalls restart, and which stack it runs on. Getting those right is the whole reason sigaction exists.

flowchart LR
  APP["sigaction(signum, act, oldact)"] --> KERN["do_sigaction()<br/>kernel/signal.c"]
  KERN --> TBL["sighand->action[signum-1]<br/>= struct k_sigaction"]
  TBL --> H["sa_handler / sa_sigaction<br/>(the function, or SIG_DFL/SIG_IGN)"]
  TBL --> M["sa_mask<br/>(extra signals blocked in handler)"]
  TBL --> F["sa_flags<br/>(SA_SIGINFO, SA_RESTART, ...)"]
  DELIV["on delivery:<br/>get_signal() reads this slot"] --> TBL

Installing and consuming a disposition. What it shows: sigaction writes one slot of the per-thread-group action table; at delivery time get_signal() reads the same slot to decide handler vs default vs ignore and how to set up the frame. The insight to take: the handler pointer is only one of three things you configure — the mask and flags are equally load-bearing, and the old racy signal() left them implicit and inconsistent.

struct sigaction — Field by Field

The userspace structure (sigaction(2)) is:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • sa_handler — the simple, one-argument handler void h(int signum), or one of the special constants SIG_DFL (default action) and SIG_IGN (ignore). Used when SA_SIGINFO is not set in sa_flags.
  • sa_sigaction — the three-argument handler void h(int signum, siginfo_t *info, void *ucontext), used when SA_SIGINFO is set. The man page warns: “On some architectures a union is involved: do not assign to both sa_handler and sa_sigaction.” On Linux sa_handler and sa_sigaction overlap in a union, so you fill exactly one and set the flag accordingly. The first argument is the signal number; the second points at the siginfo_t (below); the third is a ucontext_t * describing the interrupted machine state (the same context [[Signal Delivery and the Return to Userspace|rt_sigreturn restores]]), castable to inspect or even modify the register set that resumes after the handler.
  • sa_mask — a set of additional signals to block while this handler executes. The signal being handled is also blocked (unless SA_NODEFER), so sa_mask is for blocking other signals you don’t want interrupting this handler. Restored automatically when the handler returns.
  • sa_flags — the bit field that selects semantics; detailed below.
  • sa_restorer — “not intended for application use.” It holds the address of the return trampoline (__restore_rt in the VDSO/libc); the C library fills it in. On x86-64 a frame is rejected if SA_RESTORER isn’t set, so glibc always supplies it. You never touch this field.

The third argument to sigaction() itself, oldact, receives the previous disposition if non-NULL, so you can save and later restore a handler.

The siginfo_t Payload (SA_SIGINFO)

With SA_SIGINFO, the handler’s second argument is a siginfo_t, a large union whose meaningful members depend on the signal and on si_code. The man page lists (sigaction(2)):

siginfo_t {
    int      si_signo;     /* Signal number */
    int      si_errno;     /* An errno value */
    int      si_code;      /* Signal code: WHY it was sent */
    pid_t    si_pid;       /* Sending process ID */
    uid_t    si_uid;       /* Real UID of sending process */
    int      si_status;    /* Exit value or signal (SIGCHLD) */
    union sigval si_value; /* Value supplied with sigqueue() */
    void    *si_addr;      /* Faulting address (SIGSEGV/SIGBUS/...) */
    int      si_fd;        /* File descriptor (SIGIO) */
    int      si_pkey;      /* Protection key (SEGV_PKUERR) */
    ... /* many more, signal-specific */
}

si_signo, si_errno and si_code are defined for all signals.” si_code is the discriminator that tells you how the signal was generated and which other members are valid:

  • SI_USERkill(2); si_pid/si_uid identify the sender.
  • SI_QUEUEsigqueue(3); si_value carries the caller’s payload (this is how realtime signals pass data; see Realtime Signals).
  • SI_TIMER — a POSIX timer or setitimer/alarm fired; si_timerid/si_overrun valid.
  • SI_KERNEL — generated by the kernel itself.
  • SI_TKILLtgkill(2)/tkill(2).

For fault signals the si_code is more specific and si_addr becomes meaningful: a SIGSEGV carries SEGV_MAPERR (address not mapped) or SEGV_ACCERR (mapped but permission violation) or SEGV_PKUERR (memory-protection-key denial, with si_pkey); a SIGBUS carries BUS_ADRALN/BUS_ADRERR/BUS_OBJERR or the machine-check codes BUS_MCEERR_AR/BUS_MCEERR_AO; a SIGCHLD carries CLD_EXITED/CLD_KILLED/CLD_DUMPED/CLD_STOPPED/CLD_CONTINUED with si_pid/si_status/si_uid describing which child and how it changed. This is precisely the information you cannot get from a plain sa_handler, and the reason SA_SIGINFO exists: a one-argument handler knows that SIGSEGV happened but not where or why; the three-argument handler can read info->si_addr and info->si_code.

Uncertain

Verify: the complete member-by-member layout and the full si_code enumeration for every signal class. Reason: the siginfo_t is a union and this note lists the commonly-used members rather than every one; the man page’s full list (e.g. si_band, si_addr_lsb, si_lower/si_upper for SEGV_BNDERR, si_call_addr/si_syscall/si_arch for seccomp SIGSYS) is authoritative. To resolve: cross-check against sigaction(2) “The siginfo_t argument” section before relying on any rarely-used member. uncertain

The Flags — sa_flags Bit by Bit

The flag word is where sigaction earns its keep. From sigaction(2):

  • SA_SIGINFO — “The signal handler takes three arguments, not one.” Selects sa_sigaction over sa_handler and delivers the siginfo_t/ucontext. Required for any signal where you need the why (fault address, sender, realtime payload).
  • SA_RESTART — “Provide behavior compatible with BSD signal semantics by making certain system calls restartable across signals.” Controls whether a slow syscall interrupted by this signal transparently resumes or returns -1/EINTR. Only affects syscalls in the restartable set; many (poll, select, nanosleep, …) ignore it. The full mechanism is in Signal Delivery and the Return to Userspace and SA_RESTART and Signal-Interrupted Syscalls.
  • SA_NODEFER (obsolete synonym SA_NOMASK) — “Do not add the signal to the thread’s signal mask while the handler is executing, unless the signal is specified in act.sa_mask.” By default the handled signal is blocked during its own handler to prevent re-entry; SA_NODEFER removes that protection, allowing the same signal to interrupt its own handler (recursively). Use with care — recursive SIGSEGV is a fast route to stack exhaustion.
  • SA_RESETHAND (obsolete synonym SA_ONESHOT) — “Restore the signal action to the default upon entry to the signal handler.” The handler fires once, then the slot reverts to SIG_DFL. This is the safe realization of the old System V “reset on delivery” semantics — and critically, the kernel does the reset before the handler runs (you saw if (ka->sa.sa_flags & SA_ONESHOT) ka->sa.sa_handler = SIG_DFL; in get_signal()), so there’s no window where a second signal could be lost.
  • SA_ONSTACK — “Call the signal handler on an alternate signal stack provided by sigaltstack(2).” Essential for handling SIGSEGV from stack overflow (below).
  • SA_NOCLDSTOP — only meaningful for SIGCHLD: “do not receive notification when child processes stop or resume.” Without it, the parent’s SIGCHLD handler fires not just on child exit but on child stop/continue (SIGSTOP/SIGCONT), which is usually unwanted.
  • SA_NOCLDWAIT — only for SIGCHLD: “do not transform children into zombies when they terminate.” Setting SIGCHLD to SIG_IGN or setting SA_NOCLDWAIT tells the kernel to reap children automatically so they never become zombies — convenient for daemons that never call wait().
  • SA_EXPOSE_TAGBITS (Linux 5.11+) — normally architecture-specific pointer tag bits are cleared from si_addr; this flag preserves them. Relevant to memory-tagging (e.g. arm64 MTE) where the fault address’s tag is diagnostically useful.
  • SA_UNSUPPORTED (Linux 5.11+) — a probe bit. Historically the kernel silently kept unknown flag bits in oldact->sa_flags; since Linux 5.11 it clears unsupported bits, so an application can set SA_UNSUPPORTED, read back oldact, and detect whether a new flag it cares about is actually supported by this kernel. (The kernel explicitly guards against ever claiming to support this bit: BUILD_BUG_ON(UAPI_SA_FLAGS & SA_UNSUPPORTED); in do_sigaction().)

A Worked Example

#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
 
static volatile sig_atomic_t got_sigint = 0;
 
static void on_segv(int sig, siginfo_t *info, void *uctx) {
    /* async-signal-safe-only: write() is allowed, printf() is NOT */
    char buf[64];
    int n = snprintf(buf, sizeof buf, "SIGSEGV at %p code=%d\n",
                     info->si_addr, info->si_code);
    write(STDERR_FILENO, buf, n);   /* snprintf is not AS-safe; see note */
    _exit(139);
}
 
static void on_int(int sig) { got_sigint = 1; }
 
int main(void) {
    /* 1. Install an alternate stack so we can handle SIGSEGV from a
          blown stack, where the normal stack is unusable. */
    static char altstk[SIGSTKSZ];
    stack_t ss = { .ss_sp = altstk, .ss_size = sizeof altstk, .ss_flags = 0 };
    sigaltstack(&ss, NULL);
 
    /* 2. SIGSEGV: rich handler on the alt stack, one-shot. */
    struct sigaction sa_segv = {0};
    sa_segv.sa_sigaction = on_segv;
    sa_segv.sa_flags     = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND;
    sigemptyset(&sa_segv.sa_mask);
    sigaction(SIGSEGV, &sa_segv, NULL);
 
    /* 3. SIGINT: plain handler, restart slow syscalls, block SIGTERM
          while it runs. */
    struct sigaction sa_int = {0};
    sa_int.sa_handler = on_int;
    sa_int.sa_flags   = SA_RESTART;
    sigemptyset(&sa_int.sa_mask);
    sigaddset(&sa_int.sa_mask, SIGTERM);
    sigaction(SIGINT, &sa_int, NULL);
 
    while (!got_sigint) pause();   /* pause() is NOT restartable: returns on signal */
    return 0;
}

Line by line: sigaltstack() registers altstk as the stack handlers tagged SA_ONSTACK will run on. For SIGSEGV we set sa_sigaction (because SA_SIGINFO is set) so the handler can print info->si_addr; SA_ONSTACK runs it on the alternate stack (mandatory if the original stack overflowed); SA_RESETHAND makes it fire once then revert to the default (so a second fault dumps core normally instead of looping). For SIGINT we use the simple sa_handler, SA_RESTART so a read() interrupted by Ctrl-C resumes, and put SIGTERM in sa_mask so a SIGTERM can’t interrupt the SIGINT handler. Note the comment on snprintf: it is not on the async-signal-safe list, so this example is illustrative — a strictly-correct handler would format with hand-rolled AS-safe code (see Async-Signal-Safety and Reentrant Handlers). sig_atomic_t and volatile on got_sigint are required: it is written by the handler and read by main, and only volatile sig_atomic_t is guaranteed atomically accessible across that boundary.

Inside the Kernel — do_sigaction()

sigaction() reaches the kernel as rt_sigaction, which calls do_sigaction() (v6.12 kernel/signal.c, line 4170):

int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
    struct task_struct *p = current, *t;
    struct k_sigaction *k;
    sigset_t mask;
 
    if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
        return -EINVAL;
 
    k = &p->sighand->action[sig-1];
    spin_lock_irq(&p->sighand->siglock);
    ...
    if (act)
        act->sa.sa_flags &= UAPI_SA_FLAGS;   /* strip unknown flag bits */
    ...
    if (act) {
        sigdelsetmask(&act->sa.sa_mask,
                      sigmask(SIGKILL) | sigmask(SIGSTOP));
        *k = *act;
        /*
         * POSIX 3.3.1.3: setting a pending signal's action to SIG_IGN
         * (or SIG_DFL when the default is ignore) discards it.
         */
        if (sig_handler_ignored(sig_handler(p, sig), sig)) {
            sigemptyset(&mask);
            sigaddset(&mask, sig);
            flush_sigqueue_mask(&mask, &p->signal->shared_pending);
            for_each_thread(p, t)
                flush_sigqueue_mask(&mask, &t->pending);
        }
    }
    spin_unlock_irq(&p->sighand->siglock);
    return 0;
}

Several guarantees fall out of this code:

  • SIGKILL and SIGSTOP cannot be caught or ignored. act && sig_kernel_only(sig) returns -EINVAL for any attempt to change their disposition. The kernel must always be able to kill and stop a process.
  • You cannot block SIGKILL/SIGSTOP via sa_mask either. sigdelsetmask(&act->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP)) silently strips them from the mask you supplied, so even a handler can never run with SIGKILL blocked.
  • Unknown flag bits are cleared (act->sa.sa_flags &= UAPI_SA_FLAGS), which is the mechanism behind SA_UNSUPPORTED probing.
  • Setting SIG_IGN discards already-pending instances. Per POSIX, changing a pending signal’s disposition to ignore flushes it from both the per-process shared_pending queue and each thread’s pending queue (flush_sigqueue_mask). Without this you could install SIG_IGN and still take one more delivery of an already-queued instance.

The whole thing runs under p->sighand->siglock, the spinlock guarding the shared action table — handlers are a per-thread-group resource (threads created with CLONE_SIGHAND share one sighand_struct), so installing a handler in one thread changes it for all.

sigaltstack — A Separate Stack for the Handler

The companion call sigaltstack(2) registers an alternate stack for handlers flagged SA_ONSTACK. Its structure (sigaltstack(2)):

typedef struct {
    void  *ss_sp;     /* Base address of stack */
    int    ss_flags;  /* Flags */
    size_t ss_size;   /* Number of bytes in stack */
} stack_t;

The motivating use case, in the man page’s words: “The most common usage of an alternate signal stack is to handle the SIGSEGV signal that is generated if the space available for the standard stack is exhausted: in this case, a signal handler for SIGSEGV cannot be invoked on the standard stack; if we wish to handle it, we must use an alternate signal stack.” The logic is unavoidable: if the thread overflowed its stack, the kernel cannot push a signal frame onto that same (full) stack — setup_rt_frame() would fault and the delivery would degrade to a forced, fatal SIGSEGV (see Signal Delivery and the Return to Userspace). With SA_ONSTACK, get_sigframe() switches sp to ss_sp + ss_size instead, giving the handler clean room to run.

Flags and constants:

  • SS_ONSTACK (queried, not set) — the thread is currently running on the alt stack. “It is not possible to change the alternate signal stack if the thread is currently executing on it.” Trying to change it while on it returns EPERM.
  • SS_DISABLE — disable the alternate stack; “the kernel ignores any other flags in ss.ss_flags and the remaining fields.”
  • SS_AUTODISARM (Linux 4.7+) — “Clear the alternate signal stack settings on entry to the signal handler. When the signal handler returns, the previous alternate signal stack settings are restored.” This makes the alt stack safe to use with swapcontext()/coroutine libraries that would otherwise be confused by a still-armed alt stack. The kernel honors it in signal_delivered() (if (current->sas_ss_flags & SS_AUTODISARM) sas_ss_reset(current);).
  • MINSIGSTKSZ / SIGSTKSZ — minimum and recommended sizes. Note these became runtime values (via sysconf/getauxval) on systems with large register files (e.g. arm64 SVE); using the compile-time macro for a buffer can be too small. Size the alt stack from sysconf(_SC_SIGSTKSZ) when available.

Why sigaction Superseded signal()

The historical point is worth stating plainly because it is a common interview/debugging trap. The old signal() had two incompatible lineages (signal(2)):

  • System V semantics: on delivery, “the disposition of the signal would be reset to SIG_DFL, and the system did not block delivery of further instances of the signal.” This is racy: between entering the handler and the handler reinstalling itself, a second signal takes the default action (often: terminate). And the signal isn’t blocked during the handler, inviting recursion.
  • BSD semantics: “the signal disposition is not reset, and further instances of the signal are blocked from being delivered while the handler is executing,” and certain syscalls auto-restart.

Because “the behavior of signal() varies across UNIX versions, and has also varied historically across different versions of Linux,” portable code “should never use [it] to establish a handler.” sigaction removes the ambiguity by making every one of those choices an explicit flag (SA_RESETHAND for the reset, SA_NODEFER for non-blocking, SA_RESTART for restart). On modern Linux with glibc 2+, “the signal() wrapper function does not invoke the kernel system call. Instead, it calls sigaction(2) using flags that supply BSD semantics” — so even signal() is sigaction underneath, but you should call sigaction directly to be explicit.

There is also an ABI footnote: the original sigaction syscall used a 32-bit sigset_t that couldn’t represent realtime signals after Linux 2.2, so the real syscall is rt_sigaction, which takes an extra size_t sigsetsize; “The glibc wrapper transparently call[s] rt_sigaction()” so applications never see this.

Failure Modes and Common Mistakes

  • Calling unsafe functions in the handler. Using printf, malloc, or anything not on the async-signal-safe list can deadlock or corrupt state, because the handler interrupted the same code mid-operation. Symptom: rare, load-dependent hangs or crashes. Fix: restrict to AS-safe calls — see Async-Signal-Safety and Reentrant Handlers.
  • Non-volatile/non-sig_atomic_t flags. A flag set by a handler and read by the main loop must be volatile sig_atomic_t; otherwise the compiler may cache it in a register and the main loop never sees the change, or the write may not be atomic.
  • Forgetting SA_ONSTACK for SIGSEGV. A SIGSEGV handler that should diagnose stack overflow but isn’t on an alternate stack simply faults again and the process dies — the handler never runs. Symptom: process dies with SIGSEGV despite the handler being installed.
  • Assuming SA_RESTART covers everything. It doesn’t; poll/select/nanosleep/semop and timeout-bearing socket calls still return EINTR. Symptom: spurious EINTR from event loops even though every handler set SA_RESTART.
  • Assigning both sa_handler and sa_sigaction. They share a union on Linux; set exactly one and the matching SA_SIGINFO state. Zero-initialize the whole struct (struct sigaction sa = {0};) first to avoid garbage in sa_flags/sa_mask.
  • Expecting per-thread handlers. Dispositions are process-wide (per sighand_struct); only the signal mask is per-thread. Installing a handler in one thread changes it for the whole group.

Alternatives

  • signal(2) — only for SIG_DFL/SIG_IGN; never for installing handlers (see above).
  • signalfd — synchronous, fd-based signal reception: no handler, no reentrancy, no siginfo_t union juggling — you read() struct signalfd_siginfo records. Trade-off: the signal must be blocked first so it stays pending for the fd.
  • sigwaitinfo/sigtimedwait — synchronously wait for a (blocked) signal and receive its siginfo_t on a normal control-flow boundary, avoiding async handlers entirely.
  • pidfd + waitid — replaces SIGCHLD + SA_NOCLDSTOP handler logic with a pollable child-exit fd.

For genuinely asynchronous events the kernel can only express as signals — SIGSEGV/SIGFPE/SIGBUS faults, SIGALRM, terminal SIGINT/SIGTSTPsigaction with the right flags remains the tool.

See Also