Linux Signal Delivery

How an async notification becomes a function call in a running process.

At a Glance

End-to-End Lifecycle

From a kill(2) or hardware trap to the handler returning. Sender and receiver may be the same process.

sequenceDiagram participant S as Sender participant K as Kernel participant R as Receiver thread S->>K: kill / tgkill / hardware trap K->>K: find target task_struct K->>K: check permissions (same UID or CAP_KILL) alt signal blocked or task not runnable K->>K: set bit in pending (or enqueue sigqueue for RT) K->>K: set TIF_SIGPENDING on target else deliverable now K->>K: wake target from interruptible sleep end Note over R: runs until kernel-to-user return R->>K: syscall / IRQ / page fault returns K->>K: get_signal(): pop pending bit alt handler installed K->>R: build sigframe on user stack, set PC = handler R->>R: run handler(signo, siginfo, ucontext) R->>K: rt_sigreturn syscall K->>R: restore saved registers, resume else default action K->>K: Term / Core / Stop / Cont / Ign end

Where Signals Come From

Any of these paths end in the same place: a bit flipped in the target's pending set.

SourceExample signalsNotes
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 */
    ...
};

Standard vs Real-time Signals

Signals 1–31 are a bitmap; 34–64 are a queue.

Standard (1–31)Real-time (SIGRTMIN..SIGRTMAX)
Representationone bit in sigset_tstruct sigqueue in a list
CoalescingYes — N sends, ≤1 deliveryNo — N sends, N deliveries
Payloadbasic siginfo_t (sender PID/UID)arbitrary sigval (int or pointer)
Orderundefinedlow number first; FIFO within a number
Queue limitRLIMIT_SIGPENDING per-user
Send APIkill, tgkill, raisesigqueue, rt_sigqueueinfo

Kernel-side Dispatch

What actually runs inside the kernel between generation and handler entry.

flowchart TB A["syscall / IRQ / fault returns"] --> B{"TIF_SIGPENDING set?"} B -- no --> Z["iret to user"] B -- yes --> C["get_signal(): dequeue_signal()"] C --> D{"handler?"} D -- "SIG_DFL" --> E["default action: Term / Core / Stop / Cont / Ign"] D -- "SIG_IGN" --> F["discard"] D -- "custom" --> G["setup_rt_frame(): push ucontext on user stack"] G --> H["set PC = sa_handler/sa_sigaction, SP = frame"] H --> I["iret to handler"] I --> J["handler runs with signal blocked (unless SA_NODEFER)"] J --> K["handler returns to trampoline"] K --> L["rt_sigreturn syscall"] L --> M["restore saved regs + mask"] M --> Z

Default Actions

What happens when there's no handler installed.

ActionMeaningSignals (examples)
TermTerminate with no core file.SIGHUP, SIGINT, SIGPIPE, SIGALRM, SIGTERM, SIGUSR1, SIGUSR2, SIGKILL
CoreTerminate and dump core (subject to core(5) limits).SIGQUIT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS, SIGTRAP, SIGXCPU, SIGXFSZ
StopStop the process (all threads); reported via wait as WIFSTOPPED.SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU
ContResume a stopped process. Discards any pending stop signals.SIGCONT
IgnDiscard 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

FlagEffect
SA_SIGINFOUse the 3-arg sa_sigaction instead of the 1-arg sa_handler; gives you siginfo_t and ucontext_t.
SA_RESTARTRestart 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_NODEFERDon't block the signal while its handler runs (allow reentrant delivery).
SA_RESETHANDOne-shot: reset to SIG_DFL after the first delivery (the legacy BSD-signal behavior).
SA_ONSTACKRun 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_NOCLDSTOPOn SIGCHLD, don't send it when the child merely stopped (only on exit).
SA_NOCLDWAITOn 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:

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.

flowchart TB A["signal generated"] --> B{"directed at?"} B -- "tgkill / pthread_kill" --> T["specific TID"] B -- "kill(pid) / group signal" --> P["process-wide pending"] B -- "hardware trap" --> F["faulting thread"] P --> S{"any thread with signo unblocked?"} S -- yes --> T2["kernel picks one"] S -- no --> W["sits in shared pending until some thread unblocks"] T --> D["deliver on that thread's next userspace return"] T2 --> D F --> D

Synchronous Alternatives

Handlers are async-signal-unsafe by nature. Modern code treats signals as events to wait for, not interrupts.

APIWhat it doesGood 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

References