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.
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:
| Signal | No. | Default action | Can be caught, ignored, blocked? |
|---|---|---|---|
SIGINT (Ctrl-C) | 2 | Term: end the process | yes |
SIGQUIT (Ctrl-\) | 3 | Core: end it and write a core dump | yes |
SIGKILL | 9 | Term | no |
SIGUSR1 | 10 | Term | yes |
SIGTERM (plain kill) | 15 | Term | yes |
SIGCONT | 18 | Cont: resume if stopped | yes, but it resumes the process anyway |
SIGSTOP | 19 | Stop: pause the process | no |
SIGWINCH | 28 | Ign: do nothing | yes |
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
siginfoof the signal; - a
ucontext: every register the program had (saved inpt_regswhen it entered the kernel) and its old signal mask; - a return address,
pretcode, which is thesa_restorerthat glibc passed tort_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).
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 installed | Saved in the frame | What the program sees |
|---|---|---|
without SA_RESTART | RAX = -4 (-EINTR) | the handler runs, then read() returns -1 with errno = EINTR |
with SA_RESTART | RAX = the system call number, RIP 2 bytes back, on the syscall instruction | the handler runs, then read() simply starts again and keeps waiting |
| no handler (default Ign, or a stop and continue) | nothing | the 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.