The idea: a signal is a bit, delivered on the way back to user space

A signal is a small message to a process: "terminate", "stop", "your child died", "the window changed size". It carries no data beyond its number and a little information about who sent it. When you press Ctrl-C, run kill 4242, or a program divides by zero, the kernel sends a signal.

Two bands, user space on top and kernel space below. bash calls kill(4242, SIGTERM), which enters the kernel; the kernel sets bit 15 in the pending set of process 4242 and sets TIF_SIGPENDING, and kill returns 0. Later, when process 4242 is about to return to user mode, get_signal takes signal 15 from pending. Either the default action ends the process, or setup_rt_frame pushes a signal frame and the handler runs in user mode; the handler returns into rt_sigreturn, which restores the saved registers and the process continues where it was.
Sending only sets a bit; the kernel acts on it later, when the target is about to return to user mode.

Sending and delivering are two separate moments. kill() only marks the signal as pending in the target and returns. The target notices it the next time it goes from the kernel back to user mode. Only then does the kernel run the default action or jump to the program's handler. The page follows one signal through both moments on a single-CPU x86-64 machine: bash (pid 4100) sends it, the app (pid 4242) receives it.

Sending: kill() sets a bit and wakes the target

kill(pid, sig) is system call 62. The kernel first checks that the sender may signal the target: same user, or root. Then __send_signal sets bit sig in the target's pending set (struct sigpending). The set is a 64-bit mask plus a list of siginfo entries, one per pending signal, which record who sent it (si_pid, si_uid) and how (si_code = SI_USER for kill).

Next, complete_signal sets the flag TIF_SIGPENDING on the target and wakes it if it sleeps interruptibly, for example inside read(). If it is running on another CPU, the kernel sends that CPU an inter-processor interrupt (IPI) so that it enters the kernel soon (see Kinds of Interrupts in Linux). The page has one CPU, so the target is simply waiting for its turn. kill() returns 0 to the sender before anything has been delivered.

Delivery: on every return to user mode

Every time a task leaves the kernel, after a system call, an interrupt or a context switch, exit_to_user_mode_loop checks TIF_SIGPENDING. If it is set, get_signal takes the lowest-numbered signal in pending & ~blocked, removes it from the pending set, and looks up what the program asked for in sighand->action[sig]. A signal never cuts into user code directly: the program is always in the kernel first, through a timer interrupt or a system call. That is why a process that is not running can't react to a signal until it is scheduled again (Linux Context Switch).

Default actions and sigaction

Each signal has a disposition: the default action (SIG_DFL), ignore (SIG_IGN), or a handler function installed with sigaction() (system call 13, rt_sigaction). The eight signals the page draws:

SignalNo.Default actionCan be caught, ignored, blocked?
SIGINT (Ctrl-C)2Term: end the processyes
SIGQUIT (Ctrl-\)3Core: end it and write a core dumpyes
SIGKILL9Termno
SIGUSR110Termyes
SIGTERM (plain kill)15Termyes
SIGCONT18Cont: resume if stoppedyes, but it resumes the process anyway
SIGSTOP19Stop: pause the processno
SIGWINCH28Ign: do nothingyes

When the default action is Term, do_group_exit ends the whole process, and a shell reports exit status 128 + the signal number: 143 for SIGTERM, 137 for SIGKILL (Demo: SIGTERM, default action). The parent learns about it through SIGCHLD and wait(); that part is on the process lifecycle page.

The signal frame and rt_sigreturn

If the program installed a handler, the kernel has to run a user function in the middle of the program and then come back to the exact spot where the program was. It does this with a signal frame. setup_rt_frame pushes onto the program's own user stack, below the 128-byte red zone:

  • the siginfo of the signal;
  • a ucontext: every register the program had (saved in pt_regs when it entered the kernel) and its old signal mask;
  • a return address, pretcode, which is the sa_restorer that glibc passed to rt_sigaction: the tiny function __restore_rt.

Then it sets RIP to the handler, RSP to the frame and RDI to the signal number, and returns to user mode. The handler runs like a normal function call. When it returns, it "returns" into __restore_rt, which does mov $15, %rax; syscall: system call 15, rt_sigreturn. The kernel copies the registers and the mask back from the frame, and the program goes on as if nothing had happened (Demo: SIGTERM with a handler).

The user stack before and during the handler. Before: RSP 0x7ffd5cf8 points at main's frame and RIP is 0x401180 in the loop. During: below main's frame and a 128-byte red zone, the kernel has pushed the signal frame: pretcode pointing to __restore_rt, the siginfo for signal 15 from pid 4100, and the ucontext holding RIP 0x401180, RSP 0x7ffd5cf8 and the old mask. RSP now points at the frame (0x7ffd5778), RIP at on_sig (0x401136), RDI is 15. rt_sigreturn copies the ucontext back.
The frame holds everything needed to come back: the old registers, the old mask, and a return address that calls rt_sigreturn.

While the handler runs, the kernel also blocks that signal (plus anything in sa_mask), so the handler is not interrupted by itself. rt_sigreturn restores the old mask.

Blocking is not ignoring

sigprocmask() (system call 14, rt_sigprocmask) changes the blocked mask. A blocked signal can still be sent: it waits in the pending set, and TIF_SIGPENDING is not set for it. When the program unblocks it, the kernel sets the flag again and delivers the signal on the way out of that same sigprocmask call, before it returns (Demo: a blocked signal waits). An ignored signal is different: the kernel throws it away when it is sent, and nothing is left to deliver later.

You can see all of this in /proc/<pid>/status. Each line is a 64-bit hex mask, bit n−1 for signal n:

ShdPnd: 0000000000004000   pending for the process: bit 14 = SIGTERM (15)
SigBlk: 0000000000004000   blocked
SigIgn: 0000000000000000   set to SIG_IGN
SigCgt: 0000000000004200   caught (has a handler): SIGUSR1 (10) and SIGTERM (15)

Because the pending set is a bitmask, two identical signals that arrive while the first is still pending become one: the second kill() returns 0 but adds nothing (Demo: two SIGUSR1 become one delivery). Signals are not a counter or a queue of events. Real-time signals (SIGRTMIN to SIGRTMAX, sent with sigqueue()) are the exception: each one is queued with its own data.

Signals and blocking system calls: EINTR vs SA_RESTART

A process asleep in read() sleeps as TASK_INTERRUPTIBLE, so a signal wakes it. read() has no data yet, so it gives up with the internal code -ERESTARTSYS. What happens next depends on the handler's sa_flags:

Handler installedSaved in the frameWhat the program sees
without SA_RESTARTRAX = -4 (-EINTR)the handler runs, then read() returns -1 with errno = EINTR
with SA_RESTARTRAX = the system call number, RIP 2 bytes back, on the syscall instructionthe handler runs, then read() simply starts again and keeps waiting
no handler (default Ign, or a stop and continue)nothingthe kernel restarts the call itself

(Demos: signal during read(): EINTR, and with SA_RESTART.) A Linux System Call, Step by Step shows the same thing at the level of the registers and the system call table.

SIGKILL, SIGSTOP and SIGCONT

Two signals are reserved for the kernel and the administrator. SIGKILL always ends the process and SIGSTOP always stops it: sigaction() on them fails with EINVAL, and sigprocmask() silently leaves them out of the mask (Demo: SIGKILL cannot be caught). SIGKILL even wakes a stopped process to kill it. SIGCONT is handled the moment it is sent: it resumes a stopped process even if the program blocks or ignores it. While a process is stopped, other signals just wait in the pending set. Ctrl-Z in a terminal sends SIGTSTP, a stop signal that a program may catch.

A signal the kernel sends by itself is SIGPIPE, for a write to a pipe nobody reads any more: see How the Shell Runs ls | grep foo (yes | head -1).

Where Ctrl-C and Ctrl-Z come from, and why they reach only the foreground job, is on TTY, PTY and Job Control: What Ctrl-C Really Does.

What a handler may do

A handler can run between any two instructions of the program, even in the middle of malloc() or printf(). So it may only call async-signal-safe functions (write, _exit, and a short list in signal-safety(7)) and should only set a volatile sig_atomic_t flag, like quit = 1 here. The program checks the flag in its main loop. Larger programs avoid handlers altogether: they read signals from a file descriptor with signalfd(), or use the self-pipe trick, and handle them in their event loop.

A debugger turns signals into stops. When a traced process gets SIGTRAP from an int3 breakpoint, the kernel stops it and wakes the tracer instead of delivering the signal: How strace and gdb Work (ptrace).

What the page leaves out

The app has one thread. In a multi-threaded process, kill() puts the signal in the shared pending set (ShdPnd) and the kernel picks any thread that doesn't block it; tgkill()/pthread_kill() target one thread's own pending set (SigPnd). The page also skips real-time signal queues, sigaltstack (running the handler on another stack), SA_SIGINFO, SA_NODEFER and SA_RESETHAND, signals the CPU raises itself (SIGSEGV, SIGFPE), process groups and job control, how a core dump is written, ptrace, the FPU state in the frame, and pidfd_send_signal. Only 8 of the 64 signals are drawn, and the addresses are examples.