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 oldsignal(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 settingSIG_DFL/SIG_IGN(signal(2): “Avoid its use: use sigaction(2) instead.”).sigactionnails all of that down explicitly through astruct sigactioncarrying the handler, a mask of additional signals to block during the handler, and a flag word; withSA_SIGINFOthe handler also receives a richsiginfo_tdescribing why the signal fired. This note walks the structure field by field, the flags that change delivery semantics, andsigaltstack(2), the companion call that lets you catchSIGSEGVfrom 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 handlervoid h(int signum), or one of the special constantsSIG_DFL(default action) andSIG_IGN(ignore). Used whenSA_SIGINFOis not set insa_flags.sa_sigaction— the three-argument handlervoid h(int signum, siginfo_t *info, void *ucontext), used whenSA_SIGINFOis set. The man page warns: “On some architectures a union is involved: do not assign to bothsa_handlerandsa_sigaction.” On Linuxsa_handlerandsa_sigactionoverlap in a union, so you fill exactly one and set the flag accordingly. The first argument is the signal number; the second points at thesiginfo_t(below); the third is aucontext_t *describing the interrupted machine state (the same context [[Signal Delivery and the Return to Userspace|rt_sigreturnrestores]]), 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 (unlessSA_NODEFER), sosa_maskis 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_rtin the VDSO/libc); the C library fills it in. On x86-64 a frame is rejected ifSA_RESTORERisn’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_USER—kill(2);si_pid/si_uididentify the sender.SI_QUEUE—sigqueue(3);si_valuecarries the caller’s payload (this is how realtime signals pass data; see Realtime Signals).SI_TIMER— a POSIX timer orsetitimer/alarmfired;si_timerid/si_overrunvalid.SI_KERNEL— generated by the kernel itself.SI_TKILL—tgkill(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_codeenumeration for every signal class. Reason: thesiginfo_tis 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_upperforSEGV_BNDERR,si_call_addr/si_syscall/si_archfor seccompSIGSYS) 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.” Selectssa_sigactionoversa_handlerand delivers thesiginfo_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 synonymSA_NOMASK) — “Do not add the signal to the thread’s signal mask while the handler is executing, unless the signal is specified inact.sa_mask.” By default the handled signal is blocked during its own handler to prevent re-entry;SA_NODEFERremoves that protection, allowing the same signal to interrupt its own handler (recursively). Use with care — recursiveSIGSEGVis a fast route to stack exhaustion.SA_RESETHAND(obsolete synonymSA_ONESHOT) — “Restore the signal action to the default upon entry to the signal handler.” The handler fires once, then the slot reverts toSIG_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 sawif (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 handlingSIGSEGVfrom stack overflow (below).SA_NOCLDSTOP— only meaningful forSIGCHLD: “do not receive notification when child processes stop or resume.” Without it, the parent’sSIGCHLDhandler fires not just on child exit but on child stop/continue (SIGSTOP/SIGCONT), which is usually unwanted.SA_NOCLDWAIT— only forSIGCHLD: “do not transform children into zombies when they terminate.” SettingSIGCHLDtoSIG_IGNor settingSA_NOCLDWAITtells the kernel to reap children automatically so they never become zombies — convenient for daemons that never callwait().SA_EXPOSE_TAGBITS(Linux 5.11+) — normally architecture-specific pointer tag bits are cleared fromsi_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 inoldact->sa_flags; since Linux 5.11 it clears unsupported bits, so an application can setSA_UNSUPPORTED, read backoldact, 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);indo_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:
SIGKILLandSIGSTOPcannot be caught or ignored.act && sig_kernel_only(sig)returns-EINVALfor any attempt to change their disposition. The kernel must always be able to kill and stop a process.- You cannot block
SIGKILL/SIGSTOPviasa_maskeither.sigdelsetmask(&act->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP))silently strips them from the mask you supplied, so even a handler can never run withSIGKILLblocked. - Unknown flag bits are cleared (
act->sa.sa_flags &= UAPI_SA_FLAGS), which is the mechanism behindSA_UNSUPPORTEDprobing. - Setting
SIG_IGNdiscards already-pending instances. Per POSIX, changing a pending signal’s disposition to ignore flushes it from both the per-processshared_pendingqueue and each thread’spendingqueue (flush_sigqueue_mask). Without this you could installSIG_IGNand 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 returnsEPERM.SS_DISABLE— disable the alternate stack; “the kernel ignores any other flags inss.ss_flagsand 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 withswapcontext()/coroutine libraries that would otherwise be confused by a still-armed alt stack. The kernel honors it insignal_delivered()(if (current->sas_ss_flags & SS_AUTODISARM) sas_ss_reset(current);).MINSIGSTKSZ/SIGSTKSZ— minimum and recommended sizes. Note these became runtime values (viasysconf/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 fromsysconf(_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_tflags. A flag set by a handler and read by the main loop must bevolatile 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_ONSTACKforSIGSEGV. ASIGSEGVhandler 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 withSIGSEGVdespite the handler being installed. - Assuming
SA_RESTARTcovers everything. It doesn’t;poll/select/nanosleep/semopand timeout-bearing socket calls still returnEINTR. Symptom: spuriousEINTRfrom event loops even though every handler setSA_RESTART. - Assigning both
sa_handlerandsa_sigaction. They share a union on Linux; set exactly one and the matchingSA_SIGINFOstate. Zero-initialize the whole struct (struct sigaction sa = {0};) first to avoid garbage insa_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 forSIG_DFL/SIG_IGN; never for installing handlers (see above).- signalfd — synchronous, fd-based signal reception: no handler, no reentrancy, no
siginfo_tunion juggling — youread()struct signalfd_siginforecords. 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 itssiginfo_ton a normal control-flow boundary, avoiding async handlers entirely.- pidfd +
waitid— replacesSIGCHLD+SA_NOCLDSTOPhandler 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/SIGTSTP — sigaction with the right flags remains the tool.
See Also
- Signal Delivery and the Return to Userspace — what the kernel does with the disposition this note installs:
get_signal()/handle_signal()/the signal frame. - Linux Signals Overview — the standard signal set, default actions, and the generation/delivery model.
- Signal Sets and Masking —
sigset_t,sigprocmask, and howsa_maskplugs into the blocked set. - Async-Signal-Safety and Reentrant Handlers — the rules for what a handler may legally call.
- SA_RESTART and Signal-Interrupted Syscalls — the full restart-vs-
EINTRbehaviorSA_RESTARTselects. - Realtime Signals — queued, payload-carrying signals consumed via
SA_SIGINFO/si_value. - signalfd — the fd-based alternative to async handlers.
- Linux IPC MOC — parent map.