Linux Signal Delivery
How an async notification becomes a function call in a running process.
At a Glance
- Two phases — Generation marks a signal pending on a target task; delivery happens later, only when that task is about to return to userspace with the signal unblocked. Signals are never delivered while the target is running arbitrary kernel code.
- Many sources — hardware traps (SIGSEGV, SIGFPE, SIGBUS, SIGILL), kernel events (SIGCHLD, SIGPIPE, SIGALRM), syscalls (
kill,tgkill,pidfd_send_signal), the terminal driver (^C,^\,^Z,SIGHUPon disconnect), and timers. - Pending set + signal mask — each thread has a pending bitmap and a blocked bitmap (the signal mask). A signal sits in pending until it's both unblocked and the task is about to resume userspace.
- Standard signals coalesce — signals 1–31 are stored as a single bit: sending ten SIGUSR1s queues at most one. Real-time signals (SIGRTMIN..SIGRTMAX, 34–64) queue and preserve
siginfo. - Dispatch on kernel exit — before
ret_to_user, the kernel checksTIF_SIGPENDING, picks an unblocked pending signal, and either runs the default action or sets up a signal frame on the stack and rewrites the user PC to the handler. - Five default actions —
Term,Core,Stop,Cont,Ign. SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. - Handlers run async — they interrupt arbitrary user code, so only async-signal-safe functions are legal inside. Everything else (
malloc,printf, most of libc) is undefined behaviour. - Thread-directed vs process-directed — synchronous faults always hit the faulting thread;
kill(pid)targets the process and the kernel picks any thread with the signal unblocked;tgkill/pthread_killtarget a specific thread. - Synchronous alternatives —
signalfdturns signals into readable FDs;sigwaitinfoblocks the calling thread until one arrives. Both avoid async-signal-safety entirely.
End-to-End Lifecycle
From a kill(2) or hardware trap to the handler returning. Sender and receiver may be the same process.
Where Signals Come From
Any of these paths end in the same place: a bit flipped in the target's pending set.
| Source | Example signals | Notes |
|---|---|---|
| CPU exception | SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP | Synchronous. Raised by the CPU on a page fault, divide-by-zero, bad opcode, etc. Always delivered to the thread that trapped. |
| Kernel event | SIGCHLD, SIGPIPE, SIGALRM, SIGIO, SIGURG | Child exited; wrote to a pipe with no reader; setitimer/timer_settime fired; FD became readable with F_SETSIG. |
| Terminal driver | SIGINT (^C), SIGQUIT (^\), SIGTSTP (^Z), SIGHUP, SIGWINCH | The tty line discipline sends these to the foreground process group on control-character input or modem disconnect. |
| Explicit syscall | anything | kill(pid, sig), tgkill(tgid, tid, sig), pidfd_send_signal(pidfd, sig, ...), rt_sigqueueinfo(...). The last is the only way to choose the siginfo payload for a standard signal. |
| Self | anything | raise(3) is tgkill(tgid, gettid(), sig). abort(3) is raise(SIGABRT) with SIGABRT unblocked first. |
| Resource limit | SIGXCPU, SIGXFSZ | Sent when a process exceeds RLIMIT_CPU or RLIMIT_FSIZE. |
| OOM killer / cgroup | SIGKILL | The kernel picks a victim (OOM) or a cgroup limit is breached; SIGKILL is enqueued and cannot be ignored. |
Pending vs Blocked
Two bitmaps per thread decide when a signal can run.
struct task_struct {
...
sigset_t blocked; /* the "signal mask" — what's suppressed */
sigset_t real_blocked; /* saved across sigsuspend */
struct sigpending pending; /* this thread's queue + bitmap */
struct signal_struct *signal; /* shared, includes process-wide pending */
...
}; - Per-thread pending — set when a signal is directed at this specific TID.
- Process-wide (shared) pending — set for
kill(pid, ...); any thread with the signal unblocked can pick it up. - Blocked set — managed via
sigprocmask/pthread_sigmask. A signal sitting in pending that's also in blocked stays there until the mask clears; sending again has no effect for standard signals (it's already set). - SIGKILL & SIGSTOP — removed from the blocked mask by the kernel on every update; they cannot be suppressed.
Standard vs Real-time Signals
Signals 1–31 are a bitmap; 34–64 are a queue.
| Standard (1–31) | Real-time (SIGRTMIN..SIGRTMAX) | |
|---|---|---|
| Representation | one bit in sigset_t | struct sigqueue in a list |
| Coalescing | Yes — N sends, ≤1 delivery | No — N sends, N deliveries |
| Payload | basic siginfo_t (sender PID/UID) | arbitrary sigval (int or pointer) |
| Order | undefined | low number first; FIFO within a number |
| Queue limit | — | RLIMIT_SIGPENDING per-user |
| Send API | kill, tgkill, raise | sigqueue, rt_sigqueueinfo |
Kernel-side Dispatch
What actually runs inside the kernel between generation and handler entry.
Default Actions
What happens when there's no handler installed.
| Action | Meaning | Signals (examples) |
|---|---|---|
| Term | Terminate with no core file. | SIGHUP, SIGINT, SIGPIPE, SIGALRM, SIGTERM, SIGUSR1, SIGUSR2, SIGKILL |
| Core | Terminate and dump core (subject to core(5) limits). | SIGQUIT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS, SIGTRAP, SIGXCPU, SIGXFSZ |
| Stop | Stop the process (all threads); reported via wait as WIFSTOPPED. | SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU |
| Cont | Resume a stopped process. Discards any pending stop signals. | SIGCONT |
| Ign | Discard silently. | SIGCHLD, SIGURG, SIGWINCH |
Installing a Handler
sigaction(2) is the portable, rich API. Use this, not signal(2).
#include <signal.h>
static void on_sigint(int signo, siginfo_t *info, void *ucontext) {
/* async-signal-safe only: write(2) is fine, printf(3) is not */
const char msg[] = "caught SIGINT\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
_exit(130);
}
int main(void) {
struct sigaction sa = {
.sa_sigaction = on_sigint,
.sa_flags = SA_SIGINFO | SA_RESTART,
};
sigemptyset(&sa.sa_mask); /* extra signals to block during handler */
sigaddset(&sa.sa_mask, SIGQUIT); /* e.g. also block SIGQUIT while running */
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
pause(); /* wait for a signal */
return 0;
} sa_flags reference
| Flag | Effect |
|---|---|
SA_SIGINFO | Use the 3-arg sa_sigaction instead of the 1-arg sa_handler; gives you siginfo_t and ucontext_t. |
SA_RESTART | Restart interruptible syscalls that were in progress when the signal arrived, instead of returning EINTR. Does not apply to all syscalls (e.g. read on a socket with SO_RCVTIMEO, or poll). |
SA_NODEFER | Don't block the signal while its handler runs (allow reentrant delivery). |
SA_RESETHAND | One-shot: reset to SIG_DFL after the first delivery (the legacy BSD-signal behavior). |
SA_ONSTACK | Run the handler on the alternate stack set up via sigaltstack(2) — essential for SIGSEGV in a tight stack (otherwise you recurse on the fault). |
SA_NOCLDSTOP | On SIGCHLD, don't send it when the child merely stopped (only on exit). |
SA_NOCLDWAIT | On SIGCHLD, auto-reap zombies — the child's exit status is discarded. |
Async-Signal Safety
Handlers can interrupt any user-mode instruction, including the middle of a malloc or a stdio lock. Only call functions explicitly listed as async-signal-safe.
| Safe (selection) | Unsafe (common traps) |
|---|---|
write, read, _exit, fork, kill, signal,
sigaction, sigprocmask, pipe, dup, stat,
time, clock_gettime, alarm, pause, sem_post,
access, execve | printf/fprintf/puts (stdio locks), malloc/free (heap lock),
any C++ new/delete, localtime/ctime (static buffer),
pthread_mutex_* (not listed as safe), syslog, dlopen/dlsym,
anything that takes a lock shared with non-handler code.
|
Common idioms:
- Self-pipe trick — the handler
writes one byte to a pipe; the main loop reads from it and does the real work. Converts async delivery into synchronous I/O. Superseded bysignalfd. volatile sig_atomic_tflags — the only safe shared state. Set in the handler, checked in the main loop.- Write-and-reraise — for fatal signals, restore
SIG_DFLandraisethe signal again so that the default action (Core) still runs and shells see the correct exit status (128 + signo).
Signals and Threads
A signal's target is a task (thread), but some signals are addressed to the whole process. The kernel resolves one to the other.
- Signal dispositions are per-process — all threads share one
sigactiontable. Installing a handler affects every thread. - Signal masks are per-thread — set with
pthread_sigmask(3). A common pattern: block all signals in every thread except one dedicated “signal thread” that callssigwaitinfo. - Thread-group fan-out — SIGSTOP / SIGCONT / SIGTERM sent to any TID stop or resume every thread in the group; the kernel walks the thread list.
- Fatal signals — if the chosen action is
TermorCore, all threads in the group exit, not just the one that took the signal.
Synchronous Alternatives
Handlers are async-signal-unsafe by nature. Modern code treats signals as events to wait for, not interrupts.
| API | What it does | Good for |
|---|---|---|
signalfd(2) | Block the signal set, then read signalfd_siginfo records from an FD. | Integration with epoll / io_uring: signals become just another readable FD. |
sigwaitinfo(2) / sigtimedwait | Block the calling thread until a signal in the set is pending; return the siginfo_t. | A dedicated “signal thread” pattern; short blocking waits. |
pidfd_send_signal(2) | Send a signal to a PID referenced by a pidfd. | Eliminates the PID-reuse race: kill(pid) may kill an unrelated process if the original exited and the PID was recycled. |
ppoll / pselect | Atomically swap in a signal mask, wait on FDs, swap it back. | Avoiding the race where a signal fires between “check flag” and the poll call. |
inotify, eventfd, timerfd | FD-based notifications for things that used to require signals. | New code should reach for these first; leave SIGIO / SIGALRM alone. |
Gotchas
- EINTR — without
SA_RESTART, any slow syscall in progress returns-1witherrno = EINTR. Portable code either setsSA_RESTARTor wraps syscalls in a retry loop. - Fork & signals — a child inherits signal dispositions and the blocked mask; pending signals are not inherited. After
execve, custom handlers reset toSIG_DFL; ignored signals stay ignored. - SIG_IGN on SIGCHLD — children are auto-reaped and no zombie is created, but
waitthen returnsECHILD. SA_NOCLDWAIT is the same behaviour viasigaction. - Signals to a stopped process — queue in pending but don't run until SIGCONT arrives (except SIGKILL, which wakes and terminates immediately).
- SIGPIPE — writing to a closed pipe/socket kills most servers that forgot to
signal(SIGPIPE, SIG_IGN)or useMSG_NOSIGNAL. - Stack overflow → SIGSEGV — a plain handler runs on the overflowing stack and recurses. Install
sigaltstackand useSA_ONSTACK. - PID reuse race — between fetching a PID and calling
kill, the target can exit and its PID can be reallocated.pidfd_send_signalfixes this.
References
- signal(7) — overview: numbering, default actions, standard vs real-time, SIGKILL/SIGSTOP rules.
- sigaction(2) — the portable handler-install API and every
sa_flagsbit. - signal-safety(7) — the authoritative list of async-signal-safe functions.
- kill(2), tgkill(2), pidfd_send_signal(2) — the three ways to send.
- signalfd(2), sigwaitinfo(2) — synchronous alternatives to handlers.
- sigaltstack(2) — alternate handler stack for stack-overflow recovery.
- sigprocmask(2) / pthread_sigmask(3) — managing the blocked set per process/thread.
- kernel/signal.c — the kernel implementation:
__send_signal_locked,get_signal,do_signal,setup_rt_frame. - Documentation/admin-guide/sysctl/kernel.rst —
core_pattern,print-fatal-signals, and related knobs. - glibc manual: Signal Handling — readable tour of the POSIX API.