Signals
A signal is a notification that the kernel delivers to a process at a moment the
process did not choose. Each signal is identified by a small number and a name. The
kernel sends signals to report events: a key pressed on the terminal, a write to a pipe
with no reader, the end of a child, an invalid memory access. Processes send them to
each other with the kill system call.
In this chapter, we will learn what state the kernel keeps for signals and at which
moment a signal takes effect. We will follow Ctrl-C from the keyboard to the end of a
program, read the kernel’s structures, and see how a handler function is run. Then we
will measure signal handling from outside a process with sigfacts, and write
sigmodel, a model of the kernel’s signal state.
This chapter covers
- the common signals, what sends them, and what they do by default
- the path of Ctrl-C from the terminal to a process
- the pending mask, the blocked mask, and the action of a signal
- the kernel structures:
struct sigpending,struct sighand_struct,struct k_sigaction - why sending a signal and delivering it are separate events
- why several sends of the same signal can result in a single delivery
- how the kernel runs a handler, and what a handler may safely do
- which dispositions survive
exec - why
SIGKILLcannot be blocked, ignored or handled EINTRandSA_RESTART: a signal during a blocking system call
The common signals
Linux has 64 signals. Numbers 1 to 31 are the standard signals, each with a fixed
meaning, and numbers 32 to 64 are the real-time signals, which programs may use for
their own purposes. kill -l lists them. These are the ones a programmer meets.
| Number | Name | Sent when | Default action |
|---|---|---|---|
| 2 | SIGINT | Ctrl-C is pressed on the terminal | end the process |
| 3 | SIGQUIT | Ctrl-\ is pressed | end the process and write a core dump |
| 9 | SIGKILL | a program calls kill with it | end the process; cannot be changed |
| 11 | SIGSEGV | the process accesses memory it has no mapping for (chapter 7) | end the process and write a core dump |
| 13 | SIGPIPE | the process writes to a pipe or socket with no reader (chapter 1) | end the process |
| 15 | SIGTERM | a program calls kill; the default of the kill command | end the process |
| 17 | SIGCHLD | a child of the process ends or stops (chapter 3) | ignore |
| 19 | SIGSTOP | a program calls kill with it | stop the process; cannot be changed |
| 20 | SIGTSTP | Ctrl-Z is pressed | stop the process |
| 18 | SIGCONT | fg, bg, or kill | continue a stopped process |
For each signal a process has a disposition, also called its action: what happens when
the signal is delivered. There are 3 possibilities. The default action is the one in
the table. Ignore discards the signal. A handler is a function of the program that
the kernel calls when the signal arrives. SIGTERM is the polite request to end, which
a program can handle to save its work, and SIGKILL is the request that cannot be
refused.
From Ctrl-C to the end of a program
Suppose a program is running in a terminal, blocked in a read, and we press Ctrl-C.
- The terminal passes the byte 0x03 to the kernel’s terminal driver.
- The driver recognises it as the interrupt character and does not put it in the input
of the program. It sends
SIGINTto every process of the terminal’s foreground process group, the set of processes the shell started for the current command line. - Sending means setting a bit: the kernel sets the bit for
SIGINTin the pending mask of each of those processes. Nothing else has happened to them yet. - A process that is sleeping in a system call is woken so that it can notice.
- When the process is next about to return from the kernel to user mode, the kernel
checks its pending mask, finds
SIGINT, clears the bit, and performs the action. The default action ends the process.
SIGINT.Steps 3 and 5 are separate events. Sending a signal runs in the sender, or in the kernel on behalf of a device, and only records that the signal is wanted. Delivery runs later, in the target, at the boundary between kernel mode and user mode. Every property of signals in this chapter follows from that split.
The state behind a signal
The kernel keeps 3 pieces of signal state.
| State | Contents | Changed by |
|---|---|---|
| Pending mask | 1 bit per signal: the signal has been sent and not yet delivered | kill sets a bit; delivery clears it |
| Blocked mask | 1 bit per signal: delivery of the signal is postponed | sigprocmask |
| Action | per signal: the default, ignore, or the address of a handler function | sigaction |
Blocking a signal is different from ignoring it. An ignored signal is discarded. A blocked signal stays pending and is delivered when the program unblocks it. A program blocks a signal around a piece of code that must not be interrupted by its handler.
kill sets a pending bit. The action is performed later, in the target.These are the kernel’s definitions in Linux 5.15, from include/linux/signal_types.h,
include/linux/sched/signal.h and include/linux/sched.h, reduced to the fields we use.
struct sigpending {
struct list_head list; /* queued details of pending signals */
sigset_t signal; /* the pending mask: 64 bits */
};
struct sigaction {
__sighandler_t sa_handler; /* SIG_DFL, SIG_IGN, or a function */
unsigned long sa_flags; /* SA_RESTART, ... */
sigset_t sa_mask; /* signals blocked while the handler runs */
};
struct sighand_struct {
struct k_sigaction action[_NSIG]; /* one action per signal, _NSIG is 64 */
/* ... */
};
struct signal_struct {
struct sigpending shared_pending; /* signals sent to the process */
/* ... */
};
struct task_struct {
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked; /* the blocked mask */
struct sigpending pending; /* signals sent to this thread */
/* ... */
};
| Field | Meaning |
|---|---|
sigset_t | A set of signals as a bit mask of 64 bits. Signal n is bit n - 1. |
sigpending.signal | The pending mask. |
sighand_struct.action | The table of actions, indexed by signal number. |
sa_handler | The action: the constant SIG_DFL for the default, SIG_IGN for ignore, or the address of a handler. |
sa_flags, sa_mask | Options for a handler, set by the program with sigaction. |
task_struct.blocked | The blocked mask. Each thread has its own. |
task_struct.pending and signal_struct.shared_pending | There are 2 pending sets. A signal sent to a process with kill goes into shared_pending, and any thread of the process that does not block it may take it. A signal aimed at a single thread, such as SIGSEGV for the thread that made the invalid access, goes into that thread’s pending. |
The kernel shows these masks in /proc/<pid>/status. This is a shell after
trap '' INT and trap 'echo x' USR1:
$ grep -E 'SigPnd|ShdPnd|SigBlk|SigIgn|SigCgt' /proc/$$/status
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000002
SigCgt: 0000000000010200
SigPnd is the thread’s pending mask and ShdPnd is shared_pending. SigBlk is
blocked. SigIgn and SigCgt are derived from the action table: the signals that are
ignored and the signals that have a handler. The values are hexadecimal. 0x2 is bit 1,
signal 2, SIGINT. 0x10200 is bit 9, signal 10, SIGUSR1, and bit 16, signal 17,
SIGCHLD, which the shell handles for its own purposes.
The masks are easier to read after decoding one by hand. Take SigCgt,
0000000000010200.
- Drop the leading zeros:
0x10200. - Write each hexadecimal digit as 4 bits:
1 0000 0010 0000 0000. - Number the bits from the right, starting at 0. The bits that are 1 are bit 9 and bit 16.
- Add 1 to each: signals 10 and 17,
SIGUSR1andSIGCHLD.
0x10200 decoded bit by bit.SigIgn, 0x2, is the single bit 1, signal 2, SIGINT. A mask of all zeros, as for
SigPnd and SigBlk here, means that no signal is pending and none is blocked.
The design has direct consequences, and the measurements below confirm each of them.
- The pending mask has 1 bit per signal, so it cannot count. A signal that is sent again while its bit is set leaves the mask unchanged.
- A blocked signal stays pending for as long as it is blocked.
- A handler is an address inside the current program.
execreplaces the program, so a handled signal returns to the default action. Ignore and default are independent of the program and are kept. SIGKILLandSIGSTOPare exempt from all 3 mechanisms. The kernel guarantees that an administrator can always end or stop a process.
SIGKILL cannot be blocked, ignored or handled. A process that receives it gets no chance to clean up.
How a handler is run
A handler is a function in the program, and the kernel has to make the program call it at a moment the program did not plan for. It does so by editing the state it is about to return to.
- The process is in the kernel, about to return to user mode, with its user-mode registers saved.
- The kernel copies those saved registers to the user stack of the process, in a signal frame.
- It changes the saved program counter to the address of the handler, and arranges the
return address of the handler to point at a small piece of code that makes the
system call
sigreturn. - It returns to user mode. The process now executes the handler, as if the handler had been called at the point of interruption.
- When the handler returns,
sigreturnenters the kernel, which restores the registers from the signal frame. The program continues at the instruction where it was interrupted.
The handler can run between any 2 instructions of the program. If the program was in the
middle of malloc, with the allocator’s data structures half updated, and the handler
calls malloc too, the structures are corrupted. A handler may therefore call only
functions that are async-signal-safe, a short list that includes write and excludes
malloc and printf. The usual practice is a handler that does almost nothing: it
sets a flag, or writes a byte to a pipe that the main loop of the program reads.
The runtimes of both languages do this for us. The Go runtime installs handlers for all
signals and forwards them to the program through a channel registered with
signal.Notify. Rust libraries such as signal-hook use the pipe technique. In both,
the code we write for a signal runs in ordinary program context and may call anything.
Measuring signals
A program that changed its own signal handling would measure its language runtime as
much as the kernel. sigfacts therefore observes other processes. It starts shell
scripts that set dispositions with trap, sends them signals, and reads their masks
from /proc/<pid>/status. The Rust version uses the nix crate with the signal
feature.
$ cargo new sigfacts
$ cargo add -p sigfacts nix --features signal
mask reads a mask from the status file as a hexadecimal number. await waits until a
process has reached a given state.
Listing 5.1 The imports of
sigfacts, in Go. code/go/sigfacts/main.go
import (
"bufio"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
)
Listing 5.2 The imports of
sigfacts, in Rust. code/rust/sigfacts/src/main.rs
use std::fs;
use std::io::{self, Read};
use std::os::unix::process::ExitStatusExt;
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
Listing 5.3 Reading the signal masks of another process, in Go. code/go/sigfacts/main.go
// mask returns one of the signal masks in /proc/<pid>/status: ShdPnd
// (pending), SigBlk (blocked), SigIgn (ignored) or SigCgt (caught).
func mask(pid int, name string) (uint64, error) {
status, err := os.Open(fmt.Sprintf("/proc/%d/status", pid))
if err != nil {
return 0, err
}
defer status.Close()
for lines := bufio.NewScanner(status); lines.Scan(); {
if value, ok := strings.CutPrefix(lines.Text(), name+":"); ok {
return strconv.ParseUint(strings.TrimSpace(value), 16, 64)
}
}
return 0, fmt.Errorf("%s: not in /proc/%d/status", name, pid)
}
// has reports whether a mask contains a signal. Signal n is bit n-1.
func has(mask uint64, signal syscall.Signal) bool {
return mask&(1<<(signal-1)) != 0
}
// await polls /proc/<pid>/stat until the process has the given command name
// and state. State T is "stopped" and S is "sleeping".
func await(pid int, command string, state byte) error {
want := fmt.Sprintf("(%s) %c", command, state)
for {
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
return err
}
if strings.Contains(string(stat), want) {
return nil
}
time.Sleep(time.Millisecond)
}
}
Listing 5.4 Reading the signal masks of another process, in Rust. code/rust/sigfacts/src/main.rs
/// Returns one of the signal masks in `/proc/<pid>/status`: `ShdPnd`
/// (pending), `SigBlk` (blocked), `SigIgn` (ignored) or `SigCgt` (caught).
fn mask(pid: Pid, name: &str) -> io::Result<u64> {
let status = fs::read_to_string(format!("/proc/{pid}/status"))?;
status
.lines()
.find_map(|line| line.strip_prefix(name)?.strip_prefix(':'))
.and_then(|value| u64::from_str_radix(value.trim(), 16).ok())
.ok_or_else(|| io::ErrorKind::InvalidData.into())
}
/// Reports whether a mask contains a signal. Signal `n` is bit `n - 1`.
fn has(mask: u64, signal: Signal) -> bool {
mask & (1 << (signal as i32 - 1)) != 0
}
/// Polls `/proc/<pid>/stat` until the process has the given command name and
/// state. State `T` is "stopped" and `S` is "sleeping".
fn wait_for(pid: Pid, command: &str, state: char) -> io::Result<()> {
let want = format!("({command}) {state}");
while !fs::read_to_string(format!("/proc/{pid}/stat"))?.contains(&want) {
thread::sleep(Duration::from_millis(1));
}
Ok(())
}
/// Starts a shell script and returns the child with its process number.
fn shell(script: &str, stdout: Stdio) -> io::Result<(Child, Pid)> {
let child = Command::new("sh")
.args(["-c", script])
.stdout(stdout)
.spawn()?;
let pid = Pid::from_raw(child.id() as i32);
Ok((child, pid))
}
A stopped process does not return to user mode, so nothing is delivered to it. pending
uses that to observe the pending mask. The script installs a handler for SIGUSR1 and
stops itself. The program sends SIGUSR1 3 times, reads the mask, and lets the script
continue with SIGCONT.
Listing 5.5 Sending a signal 3 times to a stopped process, in Go. code/go/sigfacts/main.go
// pending sends SIGUSR1 3 times to a stopped process that has a handler for
// it, reads the pending mask, and counts how often the handler runs after
// the process continues.
func pending() error {
child := exec.Command("sh", "-c", `trap 'echo caught' USR1; kill -STOP $$; echo done`)
stdout, err := child.StdoutPipe()
if err != nil {
return err
}
if err := child.Start(); err != nil {
return err
}
pid := child.Process.Pid
if err := await(pid, "sh", 'T'); err != nil {
return err
}
for range 3 {
syscall.Kill(pid, syscall.SIGUSR1)
}
waiting, err := mask(pid, "ShdPnd")
if err != nil {
return err
}
syscall.Kill(pid, syscall.SIGCONT)
text, err := io.ReadAll(stdout)
if err != nil {
return err
}
fmt.Printf("SIGUSR1 sent 3 times to a stopped process: pending mask %#x\n", waiting)
fmt.Printf("after SIGCONT the handler ran %d time(s)\n", strings.Count(string(text), "caught"))
return child.Wait()
}
Listing 5.6 Sending a signal 3 times to a stopped process, in Rust. code/rust/sigfacts/src/main.rs
/// Sends `SIGUSR1` 3 times to a stopped process that has a handler for it,
/// reads the pending mask, and counts how often the handler runs after the
/// process continues.
fn pending() -> io::Result<()> {
let script = "trap 'echo caught' USR1; kill -STOP $$; echo done";
let (mut child, pid) = shell(script, Stdio::piped())?;
wait_for(pid, "sh", 'T')?;
for _ in 0..3 {
kill(pid, Signal::SIGUSR1)?;
}
let waiting = mask(pid, "ShdPnd")?;
kill(pid, Signal::SIGCONT)?;
let mut text = String::new();
if let Some(stdout) = child.stdout.as_mut() {
stdout.read_to_string(&mut text)?;
}
child.wait()?;
println!("SIGUSR1 sent 3 times to a stopped process: pending mask {waiting:#x}");
println!(
"after SIGCONT the handler ran {} time(s)",
text.matches("caught").count()
);
Ok(())
}
dispositions reads the ignored and caught masks of a shell that ignores SIGINT and
handles SIGUSR1.
Listing 5.7 The actions of a process, read from outside, in Go. code/go/sigfacts/main.go
// dispositions reads the ignored and caught masks of a shell that ignores
// SIGINT and handles SIGUSR1.
func dispositions() error {
child := exec.Command("sh", "-c", `trap '' INT; trap 'echo caught' USR1; kill -STOP $$`)
if err := child.Start(); err != nil {
return err
}
defer child.Wait()
defer child.Process.Kill()
pid := child.Process.Pid
if err := await(pid, "sh", 'T'); err != nil {
return err
}
ignored, err := mask(pid, "SigIgn")
if err != nil {
return err
}
caught, err := mask(pid, "SigCgt")
if err != nil {
return err
}
fmt.Printf("shell with 2 traps: SIGINT ignored %v, SIGUSR1 caught %v, SIGTERM ignored %v\n",
has(ignored, syscall.SIGINT), has(caught, syscall.SIGUSR1), has(ignored, syscall.SIGTERM))
return nil
}
Listing 5.8 The actions of a process, read from outside, in Rust. code/rust/sigfacts/src/main.rs
/// Reads the ignored and caught masks of a shell that ignores `SIGINT` and
/// handles `SIGUSR1`.
fn dispositions() -> io::Result<()> {
let script = "trap '' INT; trap 'echo caught' USR1; kill -STOP $$";
let (mut child, pid) = shell(script, Stdio::inherit())?;
wait_for(pid, "sh", 'T')?;
let ignored = mask(pid, "SigIgn")?;
let caught = mask(pid, "SigCgt")?;
child.kill()?;
child.wait()?;
println!(
"shell with 2 traps: SIGINT ignored {}, SIGUSR1 caught {}, SIGTERM ignored {}",
has(ignored, Signal::SIGINT),
has(caught, Signal::SIGUSR1),
has(ignored, Signal::SIGTERM)
);
Ok(())
}
acrossExec starts a shell that ignores SIGTERM, handles SIGUSR1, and then replaces
itself with sleep by exec. It reads the masks of the sleep process, sends it
SIGTERM and then SIGKILL, and reports how it ended.
Listing 5.9 Dispositions after exec, and SIGKILL, in Go. code/go/sigfacts/main.go
// acrossExec reads the masks of a process after exec, sends it a signal it
// ignores, and then SIGKILL.
func acrossExec() error {
child := exec.Command("sh", "-c", `trap '' TERM; trap 'echo caught' USR1; exec sleep 30`)
if err := child.Start(); err != nil {
return err
}
pid := child.Process.Pid
if err := await(pid, "sleep", 'S'); err != nil {
return err
}
ignored, err := mask(pid, "SigIgn")
if err != nil {
return err
}
caught, err := mask(pid, "SigCgt")
if err != nil {
return err
}
fmt.Printf("after exec: SIGTERM ignored %v, SIGUSR1 caught %v\n",
has(ignored, syscall.SIGTERM), has(caught, syscall.SIGUSR1))
syscall.Kill(pid, syscall.SIGTERM)
syscall.Kill(pid, syscall.SIGKILL)
child.Wait()
if status, ok := child.ProcessState.Sys().(syscall.WaitStatus); ok && status.Signaled() {
fmt.Printf("SIGTERM, then SIGKILL: killed by signal %d\n", status.Signal())
}
return nil
}
Listing 5.10 Dispositions after exec, and SIGKILL, in Rust. code/rust/sigfacts/src/main.rs
/// Reads the masks of a process after exec, sends it a signal it ignores, and
/// then `SIGKILL`.
fn across_exec() -> io::Result<()> {
let script = "trap '' TERM; trap 'echo caught' USR1; exec sleep 30";
let (mut child, pid) = shell(script, Stdio::inherit())?;
wait_for(pid, "sleep", 'S')?;
let ignored = mask(pid, "SigIgn")?;
let caught = mask(pid, "SigCgt")?;
println!(
"after exec: SIGTERM ignored {}, SIGUSR1 caught {}",
has(ignored, Signal::SIGTERM),
has(caught, Signal::SIGUSR1)
);
kill(pid, Signal::SIGTERM)?;
kill(pid, Signal::SIGKILL)?;
if let Some(signal) = child.wait()?.signal() {
println!("SIGTERM, then SIGKILL: killed by signal {signal}");
}
Ok(())
}
SIGUSR1 sent 3 times to a stopped process: pending mask 0x200
after SIGCONT the handler ran 1 time(s)
shell with 2 traps: SIGINT ignored true, SIGUSR1 caught true, SIGTERM ignored false
after exec: SIGTERM ignored true, SIGUSR1 caught false
SIGTERM, then SIGKILL: killed by signal 9
| Output | Conclusion |
|---|---|
pending mask 0x200 after 3 sends | 0x200 is bit 9, signal 10, SIGUSR1. The mask records that the signal is pending and holds no count. |
| the handler ran 1 time | 3 sends were merged into a single delivery. |
SIGINT ignored, SIGUSR1 caught, SIGTERM not ignored | Each signal has its own entry in the action table. |
after exec: SIGTERM ignored, SIGUSR1 not caught | Ignore survives exec. A handler is reset to the default. |
| killed by signal 9 | SIGTERM was discarded. SIGKILL ended the process. |
Line 4 explains a detail from chapter 1. A Rust program starts with
SIGPIPE ignored, and an ignored signal stays ignored across exec. Rust’s Command
therefore resets SIGPIPE to the default in each child before it calls exec, and
yes in the pipeline was ended by the signal as intended.
The merging of signals has a consequence for every program that handles SIGCHLD. A
handler for it calls wait in a loop until no ended child remains, because several
children may have ended behind a single delivery.
Modelling signal state
Low-level design: sigmodel
Field of process | Kernel field | Meaning |
|---|---|---|
pending | shared_pending.signal | mask of 64 bits; signal n is bit n - 1 |
blocked | task_struct.blocked | mask of 64 bits |
actions, restart | sighand->action[n] | default, ignore or handle; the SA_RESTART flag |
handled, killedBy | how many handlers have run; the signal that ended the process | |
call, outcome | the system call the process is blocked in; what a signal did to it |
| Operation | System call | Behaviour |
|---|---|---|
setAction(signal, action, restart) | sigaction | installs an action; fails with EINVAL for SIGKILL |
block(mask), unblock(mask) | sigprocmask | changes the blocked mask; SIGKILL cannot be added |
send(signal) | kill | discards an ignored signal, otherwise sets its pending bit |
deliver() | performs the action of each pending, unblocked signal, lowest number first |
The state of a process is the 2 masks and a table of actions.
Listing 5.11 The imports of
sigmodel, in Go. code/go/sigmodel/main.go
import (
"errors"
"fmt"
"log"
)
Listing 5.12 The imports of
sigmodel, in Rust. code/rust/sigmodel/src/main.rs
use std::fmt;
Listing 5.13 The signal state of a process, in Go. code/go/sigmodel/main.go
const (
sigKill = 9
sigUsr1 = 10
sigTerm = 15
sigChld = 17
sigLast = 64
)
type action int
const (
byDefault action = iota
ignore
handle
)
var errInvalid = errors.New("EINVAL")
// A process holds the signal state of one process. Signal n is bit n-1 of
// the masks.
type process struct {
pending uint64
blocked uint64
actions [sigLast + 1]action
restart [sigLast + 1]bool
handled int
killedBy int
// call is the system call the process is blocked in, or "" when it is
// running. outcome records what a signal did to that call.
call string
outcome string
}
func bit(signal int) uint64 {
return 1 << (signal - 1)
}
Listing 5.14 The signal state of a process, in Rust. code/rust/sigmodel/src/main.rs
const SIGKILL: usize = 9;
const SIGUSR1: usize = 10;
const SIGTERM: usize = 15;
const SIGCHLD: usize = 17;
const SIGLAST: usize = 64;
#[derive(Clone, Copy, PartialEq)]
enum Action {
Default,
Ignore,
Handle,
}
#[derive(Debug)]
struct Invalid;
impl fmt::Display for Invalid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("EINVAL")
}
}
/// The signal state of one process. Signal `n` is bit `n - 1` of the masks.
struct Process {
pending: u64,
blocked: u64,
actions: [Action; SIGLAST + 1],
restart: [bool; SIGLAST + 1],
handled: u32,
killed_by: Option<usize>,
/// The system call the process is blocked in, or `None` when it is
/// running. `outcome` records what a signal did to that call.
call: Option<&'static str>,
outcome: String,
}
fn bit(signal: usize) -> u64 {
1 << (signal - 1)
}
impl Process {
fn new(call: Option<&'static str>) -> Process {
Process {
pending: 0,
blocked: 0,
actions: [Action::Default; SIGLAST + 1],
restart: [false; SIGLAST + 1],
handled: 0,
killed_by: None,
call,
outcome: String::new(),
}
}
}
setAction and block enforce the exemption of SIGKILL. Changing its action is an
error, and a request to block it is reduced silently.
Listing 5.15 Installing actions and changing the blocked mask, in Go. code/go/sigmodel/main.go
// setAction installs an action for a signal. restart is the SA_RESTART flag
// of a handler. The action of SIGKILL cannot be changed.
func (p *process) setAction(signal int, a action, restart bool) error {
if signal == sigKill {
return errInvalid
}
p.actions[signal], p.restart[signal] = a, restart
return nil
}
// block adds signals to the blocked mask. SIGKILL is removed from the
// request without an error, as the kernel does.
func (p *process) block(mask uint64) {
p.blocked |= mask &^ bit(sigKill)
}
// unblock removes signals from the blocked mask and delivers what was
// waiting for it.
func (p *process) unblock(mask uint64) {
p.blocked &^= mask
p.deliver()
}
Listing 5.16 Installing actions and changing the blocked mask, in Rust. code/rust/sigmodel/src/main.rs
impl Process {
/// Installs an action for a signal. `restart` is the `SA_RESTART` flag of
/// a handler. The action of `SIGKILL` cannot be changed.
fn set_action(&mut self, signal: usize, action: Action, restart: bool) -> Result<(), Invalid> {
if signal == SIGKILL {
return Err(Invalid);
}
self.actions[signal] = action;
self.restart[signal] = restart;
Ok(())
}
/// Adds signals to the blocked mask. `SIGKILL` is removed from the
/// request without an error, as the kernel does.
fn block(&mut self, mask: u64) {
self.blocked |= mask & !bit(SIGKILL);
}
/// Removes signals from the blocked mask and delivers what was waiting
/// for it.
fn unblock(&mut self, mask: u64) {
self.blocked &= !mask;
self.deliver();
}
}
send is the half that runs in the sender. A signal that the target ignores is
discarded at this point and does not become pending, unless it is blocked: a blocked
signal is kept, because its action may have changed by the time it is unblocked.
Setting a bit that is already set has no effect, which is the merging we measured.
Listing 5.17 Sending: setting a pending bit, in Go. code/go/sigmodel/main.go
// ignored reports whether a signal would be discarded on delivery: its
// action is ignore, or its action is the default and the default is to
// ignore, which is the case for SIGCHLD.
func (p *process) ignored(signal int) bool {
return p.actions[signal] == ignore || p.actions[signal] == byDefault && signal == sigChld
}
// send is the sender's half. It discards a signal that the target ignores
// and does not block. Otherwise it sets the pending bit, which is already
// set when the same signal is waiting.
func (p *process) send(signal int) {
if p.ignored(signal) && p.blocked&bit(signal) == 0 {
return
}
p.pending |= bit(signal)
}
Listing 5.18 Sending: setting a pending bit, in Rust. code/rust/sigmodel/src/main.rs
impl Process {
/// Reports whether a signal would be discarded on delivery: its action is
/// ignore, or its action is the default and the default is to ignore,
/// which is the case for `SIGCHLD`.
fn ignored(&self, signal: usize) -> bool {
match self.actions[signal] {
Action::Ignore => true,
Action::Default => signal == SIGCHLD,
Action::Handle => false,
}
}
/// The sender's half. It discards a signal that the target ignores and
/// does not block. Otherwise it sets the pending bit, which is already
/// set when the same signal is waiting.
fn send(&mut self, signal: usize) {
if self.ignored(signal) && self.blocked & bit(signal) == 0 {
return;
}
self.pending |= bit(signal);
}
}
deliver is the half that runs in the target. It considers the signals that are pending
and not blocked. For a handler it also decides the fate of a system call that the
process was blocked in.
Listing 5.19 Delivering: performing the action, in Go. code/go/sigmodel/main.go
// deliver is the target's half. The kernel runs it when the process is about
// to return to user mode. It takes each pending signal that is not blocked,
// lowest number first, and performs its action.
func (p *process) deliver() {
for signal := 1; signal <= sigLast && p.killedBy == 0; signal++ {
if p.pending&^p.blocked&bit(signal) == 0 {
continue
}
p.pending &^= bit(signal)
switch {
case p.actions[signal] == handle:
p.handled++
p.interrupt(signal)
case !p.ignored(signal):
p.killedBy = signal
}
}
}
// interrupt decides what happens to a blocking system call when a handler
// has run: the call is restarted, or it fails with EINTR.
func (p *process) interrupt(signal int) {
if p.call == "" {
return
}
if p.restart[signal] {
p.outcome = "the " + p.call + " is restarted"
return
}
p.outcome = "the " + p.call + " fails with EINTR"
p.call = ""
}
Listing 5.20 Delivering: performing the action, in Rust. code/rust/sigmodel/src/main.rs
impl Process {
/// The target's half. The kernel runs it when the process is about to
/// return to user mode. It takes each pending signal that is not blocked,
/// lowest number first, and performs its action.
fn deliver(&mut self) {
for signal in 1..=SIGLAST {
if self.killed_by.is_some() {
break;
}
if self.pending & !self.blocked & bit(signal) == 0 {
continue;
}
self.pending &= !bit(signal);
if self.actions[signal] == Action::Handle {
self.handled += 1;
self.interrupt(signal);
} else if !self.ignored(signal) {
self.killed_by = Some(signal);
}
}
}
/// Decides what happens to a blocking system call when a handler has run:
/// the call is restarted, or it fails with `EINTR`.
fn interrupt(&mut self, signal: usize) {
let Some(call) = self.call else {
return;
};
if self.restart[signal] {
self.outcome = format!("the {call} is restarted");
return;
}
self.outcome = format!("the {call} fails with EINTR");
self.call = None;
}
}
A signal during a blocking call
A process that is blocked in a system call is inside the kernel and is not about to
return to user mode. If signals waited for the call to finish, a process blocked in a
read on a terminal could not be interrupted by Ctrl-C. The kernel therefore wakes the
process, abandons the call, and runs the handler. After the handler returns there are 2
possible outcomes, selected by a flag given to sigaction when the handler was
installed.
Without SA_RESTART, the call returns the error EINTR, “interrupted system call”,
and the program has to call it again. A loop that retries on EINTR around every
blocking call is the standard defence. Go’s standard library and Rust’s std::io
helpers such as read_to_end contain that loop.
Listing 5.21 The trace, in Go. code/go/sigmodel/main.go
func coalescing() error {
p := &process{}
if err := p.setAction(sigUsr1, handle, false); err != nil {
return err
}
p.block(bit(sigUsr1))
for range 3 {
p.send(sigUsr1)
p.deliver()
}
fmt.Printf("SIGUSR1 blocked and sent 3 times: pending mask %#x\n", p.pending)
p.unblock(bit(sigUsr1))
fmt.Printf("after unblocking the handler ran %d time(s)\n", p.handled)
return nil
}
func ignoring() error {
p := &process{}
if err := p.setAction(sigTerm, ignore, false); err != nil {
return err
}
p.send(sigTerm)
fmt.Printf("SIGTERM ignored and sent: pending mask %#x\n", p.pending)
fmt.Printf("installing a handler for SIGKILL: %v\n", p.setAction(sigKill, handle, false))
p.block(^uint64(0))
p.send(sigKill)
p.deliver()
fmt.Printf("every signal blocked, SIGKILL sent: killed by signal %d\n", p.killedBy)
return nil
}
func interrupting(restart bool) error {
p := &process{call: "read"}
if err := p.setAction(sigUsr1, handle, restart); err != nil {
return err
}
p.send(sigUsr1)
p.deliver()
fmt.Printf("handler runs during a blocking read, SA_RESTART %v: %s\n", restart, p.outcome)
return nil
}
func main() {
steps := []func() error{
coalescing,
ignoring,
func() error { return interrupting(false) },
func() error { return interrupting(true) },
}
for _, step := range steps {
if err := step(); err != nil {
log.Fatal(err)
}
}
}
Listing 5.22 The trace, in Rust. code/rust/sigmodel/src/main.rs
fn coalescing() -> Result<(), Invalid> {
let mut process = Process::new(None);
process.set_action(SIGUSR1, Action::Handle, false)?;
process.block(bit(SIGUSR1));
for _ in 0..3 {
process.send(SIGUSR1);
process.deliver();
}
println!(
"SIGUSR1 blocked and sent 3 times: pending mask {:#x}",
process.pending
);
process.unblock(bit(SIGUSR1));
println!(
"after unblocking the handler ran {} time(s)",
process.handled
);
Ok(())
}
fn ignoring() -> Result<(), Invalid> {
let mut process = Process::new(None);
process.set_action(SIGTERM, Action::Ignore, false)?;
process.send(SIGTERM);
println!(
"SIGTERM ignored and sent: pending mask {:#x}",
process.pending
);
if let Err(error) = process.set_action(SIGKILL, Action::Handle, false) {
println!("installing a handler for SIGKILL: {error}");
}
process.block(u64::MAX);
process.send(SIGKILL);
process.deliver();
if let Some(signal) = process.killed_by {
println!("every signal blocked, SIGKILL sent: killed by signal {signal}");
}
Ok(())
}
fn interrupting(restart: bool) -> Result<(), Invalid> {
let mut process = Process::new(Some("read"));
process.set_action(SIGUSR1, Action::Handle, restart)?;
process.send(SIGUSR1);
process.deliver();
println!(
"handler runs during a blocking read, SA_RESTART {restart}: {}",
process.outcome
);
Ok(())
}
fn main() -> Result<(), Invalid> {
coalescing()?;
ignoring()?;
interrupting(false)?;
interrupting(true)
}
SIGUSR1 blocked and sent 3 times: pending mask 0x200
after unblocking the handler ran 1 time(s)
SIGTERM ignored and sent: pending mask 0x0
installing a handler for SIGKILL: EINVAL
every signal blocked, SIGKILL sent: killed by signal 9
handler runs during a blocking read, SA_RESTART false: the read fails with EINTR
handler runs during a blocking read, SA_RESTART true: the read is restarted
Lines 1 and 2 reproduce the pending mask and the single delivery that sigfacts
measured.
Correspondence with the kernel source. __send_signal in kernel/signal.c sets the
bit, and prepare_signal discards ignored signals. Delivery starts in get_signal,
called on the way back to user mode, and the signal frame of an arm64 process is built
by setup_rt_frame in arch/arm64/kernel/signal.c. Real-time signals, numbers 32 to
64, are an exception to merging: the kernel queues each send of those separately, in
the list of struct sigpending.
Inspecting signals on a live system
| Command | Shows |
|---|---|
kill -l | the names and numbers of all signals |
grep Sig /proc/<pid>/status | the pending, blocked, ignored and caught masks of a process |
kill -TERM <pid>, kill -KILL <pid> | sends a signal to a process |
kill -STOP <pid>, kill -CONT <pid> | stops and continues a process |
strace -e trace=signal -p <pid> | the signals a process receives and the signal system calls it makes |
A process that does not react to kill is diagnosed with the second command. If
SIGTERM, bit 14, is set in SigIgn or SigBlk, the process has ignored or blocked
it, and only SIGKILL will end it.
The complete programs
Listing 5.23 The complete
sigfacts, in Go. code/go/sigfacts/main.go
// Command sigfacts measures signal handling from outside a process. It starts
// shell scripts that set signal dispositions, sends them signals, and reads
// their signal masks from /proc/<pid>/status.
//
// Run it from code/go with:
//
// go run ./sigfacts
package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
)
// mask returns one of the signal masks in /proc/<pid>/status: ShdPnd
// (pending), SigBlk (blocked), SigIgn (ignored) or SigCgt (caught).
func mask(pid int, name string) (uint64, error) {
status, err := os.Open(fmt.Sprintf("/proc/%d/status", pid))
if err != nil {
return 0, err
}
defer status.Close()
for lines := bufio.NewScanner(status); lines.Scan(); {
if value, ok := strings.CutPrefix(lines.Text(), name+":"); ok {
return strconv.ParseUint(strings.TrimSpace(value), 16, 64)
}
}
return 0, fmt.Errorf("%s: not in /proc/%d/status", name, pid)
}
// has reports whether a mask contains a signal. Signal n is bit n-1.
func has(mask uint64, signal syscall.Signal) bool {
return mask&(1<<(signal-1)) != 0
}
// await polls /proc/<pid>/stat until the process has the given command name
// and state. State T is "stopped" and S is "sleeping".
func await(pid int, command string, state byte) error {
want := fmt.Sprintf("(%s) %c", command, state)
for {
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
return err
}
if strings.Contains(string(stat), want) {
return nil
}
time.Sleep(time.Millisecond)
}
}
// pending sends SIGUSR1 3 times to a stopped process that has a handler for
// it, reads the pending mask, and counts how often the handler runs after
// the process continues.
func pending() error {
child := exec.Command("sh", "-c", `trap 'echo caught' USR1; kill -STOP $$; echo done`)
stdout, err := child.StdoutPipe()
if err != nil {
return err
}
if err := child.Start(); err != nil {
return err
}
pid := child.Process.Pid
if err := await(pid, "sh", 'T'); err != nil {
return err
}
for range 3 {
syscall.Kill(pid, syscall.SIGUSR1)
}
waiting, err := mask(pid, "ShdPnd")
if err != nil {
return err
}
syscall.Kill(pid, syscall.SIGCONT)
text, err := io.ReadAll(stdout)
if err != nil {
return err
}
fmt.Printf("SIGUSR1 sent 3 times to a stopped process: pending mask %#x\n", waiting)
fmt.Printf("after SIGCONT the handler ran %d time(s)\n", strings.Count(string(text), "caught"))
return child.Wait()
}
// dispositions reads the ignored and caught masks of a shell that ignores
// SIGINT and handles SIGUSR1.
func dispositions() error {
child := exec.Command("sh", "-c", `trap '' INT; trap 'echo caught' USR1; kill -STOP $$`)
if err := child.Start(); err != nil {
return err
}
defer child.Wait()
defer child.Process.Kill()
pid := child.Process.Pid
if err := await(pid, "sh", 'T'); err != nil {
return err
}
ignored, err := mask(pid, "SigIgn")
if err != nil {
return err
}
caught, err := mask(pid, "SigCgt")
if err != nil {
return err
}
fmt.Printf("shell with 2 traps: SIGINT ignored %v, SIGUSR1 caught %v, SIGTERM ignored %v\n",
has(ignored, syscall.SIGINT), has(caught, syscall.SIGUSR1), has(ignored, syscall.SIGTERM))
return nil
}
// acrossExec reads the masks of a process after exec, sends it a signal it
// ignores, and then SIGKILL.
func acrossExec() error {
child := exec.Command("sh", "-c", `trap '' TERM; trap 'echo caught' USR1; exec sleep 30`)
if err := child.Start(); err != nil {
return err
}
pid := child.Process.Pid
if err := await(pid, "sleep", 'S'); err != nil {
return err
}
ignored, err := mask(pid, "SigIgn")
if err != nil {
return err
}
caught, err := mask(pid, "SigCgt")
if err != nil {
return err
}
fmt.Printf("after exec: SIGTERM ignored %v, SIGUSR1 caught %v\n",
has(ignored, syscall.SIGTERM), has(caught, syscall.SIGUSR1))
syscall.Kill(pid, syscall.SIGTERM)
syscall.Kill(pid, syscall.SIGKILL)
child.Wait()
if status, ok := child.ProcessState.Sys().(syscall.WaitStatus); ok && status.Signaled() {
fmt.Printf("SIGTERM, then SIGKILL: killed by signal %d\n", status.Signal())
}
return nil
}
func main() {
for _, experiment := range []func() error{pending, dispositions, acrossExec} {
if err := experiment(); err != nil {
log.Fatal(err)
}
}
}
Listing 5.24 The complete
sigfacts, in Rust. code/rust/sigfacts/src/main.rs
//! Measures signal handling from outside a process. It starts shell scripts
//! that set signal dispositions, sends them signals, and reads their signal
//! masks from `/proc/<pid>/status`.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p sigfacts
//! ```
use std::fs;
use std::io::{self, Read};
use std::os::unix::process::ExitStatusExt;
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
/// Returns one of the signal masks in `/proc/<pid>/status`: `ShdPnd`
/// (pending), `SigBlk` (blocked), `SigIgn` (ignored) or `SigCgt` (caught).
fn mask(pid: Pid, name: &str) -> io::Result<u64> {
let status = fs::read_to_string(format!("/proc/{pid}/status"))?;
status
.lines()
.find_map(|line| line.strip_prefix(name)?.strip_prefix(':'))
.and_then(|value| u64::from_str_radix(value.trim(), 16).ok())
.ok_or_else(|| io::ErrorKind::InvalidData.into())
}
/// Reports whether a mask contains a signal. Signal `n` is bit `n - 1`.
fn has(mask: u64, signal: Signal) -> bool {
mask & (1 << (signal as i32 - 1)) != 0
}
/// Polls `/proc/<pid>/stat` until the process has the given command name and
/// state. State `T` is "stopped" and `S` is "sleeping".
fn wait_for(pid: Pid, command: &str, state: char) -> io::Result<()> {
let want = format!("({command}) {state}");
while !fs::read_to_string(format!("/proc/{pid}/stat"))?.contains(&want) {
thread::sleep(Duration::from_millis(1));
}
Ok(())
}
/// Starts a shell script and returns the child with its process number.
fn shell(script: &str, stdout: Stdio) -> io::Result<(Child, Pid)> {
let child = Command::new("sh")
.args(["-c", script])
.stdout(stdout)
.spawn()?;
let pid = Pid::from_raw(child.id() as i32);
Ok((child, pid))
}
/// Sends `SIGUSR1` 3 times to a stopped process that has a handler for it,
/// reads the pending mask, and counts how often the handler runs after the
/// process continues.
fn pending() -> io::Result<()> {
let script = "trap 'echo caught' USR1; kill -STOP $$; echo done";
let (mut child, pid) = shell(script, Stdio::piped())?;
wait_for(pid, "sh", 'T')?;
for _ in 0..3 {
kill(pid, Signal::SIGUSR1)?;
}
let waiting = mask(pid, "ShdPnd")?;
kill(pid, Signal::SIGCONT)?;
let mut text = String::new();
if let Some(stdout) = child.stdout.as_mut() {
stdout.read_to_string(&mut text)?;
}
child.wait()?;
println!("SIGUSR1 sent 3 times to a stopped process: pending mask {waiting:#x}");
println!(
"after SIGCONT the handler ran {} time(s)",
text.matches("caught").count()
);
Ok(())
}
/// Reads the ignored and caught masks of a shell that ignores `SIGINT` and
/// handles `SIGUSR1`.
fn dispositions() -> io::Result<()> {
let script = "trap '' INT; trap 'echo caught' USR1; kill -STOP $$";
let (mut child, pid) = shell(script, Stdio::inherit())?;
wait_for(pid, "sh", 'T')?;
let ignored = mask(pid, "SigIgn")?;
let caught = mask(pid, "SigCgt")?;
child.kill()?;
child.wait()?;
println!(
"shell with 2 traps: SIGINT ignored {}, SIGUSR1 caught {}, SIGTERM ignored {}",
has(ignored, Signal::SIGINT),
has(caught, Signal::SIGUSR1),
has(ignored, Signal::SIGTERM)
);
Ok(())
}
/// Reads the masks of a process after exec, sends it a signal it ignores, and
/// then `SIGKILL`.
fn across_exec() -> io::Result<()> {
let script = "trap '' TERM; trap 'echo caught' USR1; exec sleep 30";
let (mut child, pid) = shell(script, Stdio::inherit())?;
wait_for(pid, "sleep", 'S')?;
let ignored = mask(pid, "SigIgn")?;
let caught = mask(pid, "SigCgt")?;
println!(
"after exec: SIGTERM ignored {}, SIGUSR1 caught {}",
has(ignored, Signal::SIGTERM),
has(caught, Signal::SIGUSR1)
);
kill(pid, Signal::SIGTERM)?;
kill(pid, Signal::SIGKILL)?;
if let Some(signal) = child.wait()?.signal() {
println!("SIGTERM, then SIGKILL: killed by signal {signal}");
}
Ok(())
}
fn main() -> io::Result<()> {
pending()?;
dispositions()?;
across_exec()
}
Listing 5.25 The complete
sigmodel, in Go. code/go/sigmodel/main.go
// Command sigmodel models the signal state the kernel keeps for a process: a
// pending mask, a blocked mask, and an action for each signal. It separates
// sending a signal from delivering it, and shows what a delivered signal
// does to a process that is blocked in a system call.
//
// Run it from code/go with:
//
// go run ./sigmodel
package main
import (
"errors"
"fmt"
"log"
)
const (
sigKill = 9
sigUsr1 = 10
sigTerm = 15
sigChld = 17
sigLast = 64
)
type action int
const (
byDefault action = iota
ignore
handle
)
var errInvalid = errors.New("EINVAL")
// A process holds the signal state of one process. Signal n is bit n-1 of
// the masks.
type process struct {
pending uint64
blocked uint64
actions [sigLast + 1]action
restart [sigLast + 1]bool
handled int
killedBy int
// call is the system call the process is blocked in, or "" when it is
// running. outcome records what a signal did to that call.
call string
outcome string
}
func bit(signal int) uint64 {
return 1 << (signal - 1)
}
// setAction installs an action for a signal. restart is the SA_RESTART flag
// of a handler. The action of SIGKILL cannot be changed.
func (p *process) setAction(signal int, a action, restart bool) error {
if signal == sigKill {
return errInvalid
}
p.actions[signal], p.restart[signal] = a, restart
return nil
}
// block adds signals to the blocked mask. SIGKILL is removed from the
// request without an error, as the kernel does.
func (p *process) block(mask uint64) {
p.blocked |= mask &^ bit(sigKill)
}
// unblock removes signals from the blocked mask and delivers what was
// waiting for it.
func (p *process) unblock(mask uint64) {
p.blocked &^= mask
p.deliver()
}
// ignored reports whether a signal would be discarded on delivery: its
// action is ignore, or its action is the default and the default is to
// ignore, which is the case for SIGCHLD.
func (p *process) ignored(signal int) bool {
return p.actions[signal] == ignore || p.actions[signal] == byDefault && signal == sigChld
}
// send is the sender's half. It discards a signal that the target ignores
// and does not block. Otherwise it sets the pending bit, which is already
// set when the same signal is waiting.
func (p *process) send(signal int) {
if p.ignored(signal) && p.blocked&bit(signal) == 0 {
return
}
p.pending |= bit(signal)
}
// deliver is the target's half. The kernel runs it when the process is about
// to return to user mode. It takes each pending signal that is not blocked,
// lowest number first, and performs its action.
func (p *process) deliver() {
for signal := 1; signal <= sigLast && p.killedBy == 0; signal++ {
if p.pending&^p.blocked&bit(signal) == 0 {
continue
}
p.pending &^= bit(signal)
switch {
case p.actions[signal] == handle:
p.handled++
p.interrupt(signal)
case !p.ignored(signal):
p.killedBy = signal
}
}
}
// interrupt decides what happens to a blocking system call when a handler
// has run: the call is restarted, or it fails with EINTR.
func (p *process) interrupt(signal int) {
if p.call == "" {
return
}
if p.restart[signal] {
p.outcome = "the " + p.call + " is restarted"
return
}
p.outcome = "the " + p.call + " fails with EINTR"
p.call = ""
}
func coalescing() error {
p := &process{}
if err := p.setAction(sigUsr1, handle, false); err != nil {
return err
}
p.block(bit(sigUsr1))
for range 3 {
p.send(sigUsr1)
p.deliver()
}
fmt.Printf("SIGUSR1 blocked and sent 3 times: pending mask %#x\n", p.pending)
p.unblock(bit(sigUsr1))
fmt.Printf("after unblocking the handler ran %d time(s)\n", p.handled)
return nil
}
func ignoring() error {
p := &process{}
if err := p.setAction(sigTerm, ignore, false); err != nil {
return err
}
p.send(sigTerm)
fmt.Printf("SIGTERM ignored and sent: pending mask %#x\n", p.pending)
fmt.Printf("installing a handler for SIGKILL: %v\n", p.setAction(sigKill, handle, false))
p.block(^uint64(0))
p.send(sigKill)
p.deliver()
fmt.Printf("every signal blocked, SIGKILL sent: killed by signal %d\n", p.killedBy)
return nil
}
func interrupting(restart bool) error {
p := &process{call: "read"}
if err := p.setAction(sigUsr1, handle, restart); err != nil {
return err
}
p.send(sigUsr1)
p.deliver()
fmt.Printf("handler runs during a blocking read, SA_RESTART %v: %s\n", restart, p.outcome)
return nil
}
func main() {
steps := []func() error{
coalescing,
ignoring,
func() error { return interrupting(false) },
func() error { return interrupting(true) },
}
for _, step := range steps {
if err := step(); err != nil {
log.Fatal(err)
}
}
}
Listing 5.26 The complete
sigmodel, in Rust. code/rust/sigmodel/src/main.rs
//! Models the signal state the kernel keeps for a process: a pending mask, a
//! blocked mask, and an action for each signal. It separates sending a signal
//! from delivering it, and shows what a delivered signal does to a process
//! that is blocked in a system call.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p sigmodel
//! ```
use std::fmt;
const SIGKILL: usize = 9;
const SIGUSR1: usize = 10;
const SIGTERM: usize = 15;
const SIGCHLD: usize = 17;
const SIGLAST: usize = 64;
#[derive(Clone, Copy, PartialEq)]
enum Action {
Default,
Ignore,
Handle,
}
#[derive(Debug)]
struct Invalid;
impl fmt::Display for Invalid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("EINVAL")
}
}
/// The signal state of one process. Signal `n` is bit `n - 1` of the masks.
struct Process {
pending: u64,
blocked: u64,
actions: [Action; SIGLAST + 1],
restart: [bool; SIGLAST + 1],
handled: u32,
killed_by: Option<usize>,
/// The system call the process is blocked in, or `None` when it is
/// running. `outcome` records what a signal did to that call.
call: Option<&'static str>,
outcome: String,
}
fn bit(signal: usize) -> u64 {
1 << (signal - 1)
}
impl Process {
fn new(call: Option<&'static str>) -> Process {
Process {
pending: 0,
blocked: 0,
actions: [Action::Default; SIGLAST + 1],
restart: [false; SIGLAST + 1],
handled: 0,
killed_by: None,
call,
outcome: String::new(),
}
}
}
impl Process {
/// Installs an action for a signal. `restart` is the `SA_RESTART` flag of
/// a handler. The action of `SIGKILL` cannot be changed.
fn set_action(&mut self, signal: usize, action: Action, restart: bool) -> Result<(), Invalid> {
if signal == SIGKILL {
return Err(Invalid);
}
self.actions[signal] = action;
self.restart[signal] = restart;
Ok(())
}
/// Adds signals to the blocked mask. `SIGKILL` is removed from the
/// request without an error, as the kernel does.
fn block(&mut self, mask: u64) {
self.blocked |= mask & !bit(SIGKILL);
}
/// Removes signals from the blocked mask and delivers what was waiting
/// for it.
fn unblock(&mut self, mask: u64) {
self.blocked &= !mask;
self.deliver();
}
}
impl Process {
/// Reports whether a signal would be discarded on delivery: its action is
/// ignore, or its action is the default and the default is to ignore,
/// which is the case for `SIGCHLD`.
fn ignored(&self, signal: usize) -> bool {
match self.actions[signal] {
Action::Ignore => true,
Action::Default => signal == SIGCHLD,
Action::Handle => false,
}
}
/// The sender's half. It discards a signal that the target ignores and
/// does not block. Otherwise it sets the pending bit, which is already
/// set when the same signal is waiting.
fn send(&mut self, signal: usize) {
if self.ignored(signal) && self.blocked & bit(signal) == 0 {
return;
}
self.pending |= bit(signal);
}
}
impl Process {
/// The target's half. The kernel runs it when the process is about to
/// return to user mode. It takes each pending signal that is not blocked,
/// lowest number first, and performs its action.
fn deliver(&mut self) {
for signal in 1..=SIGLAST {
if self.killed_by.is_some() {
break;
}
if self.pending & !self.blocked & bit(signal) == 0 {
continue;
}
self.pending &= !bit(signal);
if self.actions[signal] == Action::Handle {
self.handled += 1;
self.interrupt(signal);
} else if !self.ignored(signal) {
self.killed_by = Some(signal);
}
}
}
/// Decides what happens to a blocking system call when a handler has run:
/// the call is restarted, or it fails with `EINTR`.
fn interrupt(&mut self, signal: usize) {
let Some(call) = self.call else {
return;
};
if self.restart[signal] {
self.outcome = format!("the {call} is restarted");
return;
}
self.outcome = format!("the {call} fails with EINTR");
self.call = None;
}
}
fn coalescing() -> Result<(), Invalid> {
let mut process = Process::new(None);
process.set_action(SIGUSR1, Action::Handle, false)?;
process.block(bit(SIGUSR1));
for _ in 0..3 {
process.send(SIGUSR1);
process.deliver();
}
println!(
"SIGUSR1 blocked and sent 3 times: pending mask {:#x}",
process.pending
);
process.unblock(bit(SIGUSR1));
println!(
"after unblocking the handler ran {} time(s)",
process.handled
);
Ok(())
}
fn ignoring() -> Result<(), Invalid> {
let mut process = Process::new(None);
process.set_action(SIGTERM, Action::Ignore, false)?;
process.send(SIGTERM);
println!(
"SIGTERM ignored and sent: pending mask {:#x}",
process.pending
);
if let Err(error) = process.set_action(SIGKILL, Action::Handle, false) {
println!("installing a handler for SIGKILL: {error}");
}
process.block(u64::MAX);
process.send(SIGKILL);
process.deliver();
if let Some(signal) = process.killed_by {
println!("every signal blocked, SIGKILL sent: killed by signal {signal}");
}
Ok(())
}
fn interrupting(restart: bool) -> Result<(), Invalid> {
let mut process = Process::new(Some("read"));
process.set_action(SIGUSR1, Action::Handle, restart)?;
process.send(SIGUSR1);
process.deliver();
println!(
"handler runs during a blocking read, SA_RESTART {restart}: {}",
process.outcome
);
Ok(())
}
fn main() -> Result<(), Invalid> {
coalescing()?;
ignoring()?;
interrupting(false)?;
interrupting(true)
}
Summary
- A signal is sent by setting a bit in the pending mask of the target. Its action is performed later, when the target is about to return to user mode and the signal is not blocked.
- Each signal has an action: the default, ignore, or a handler function. The actions are
in
struct sighand_struct, and the masks are visible in/proc/<pid>/status. - The pending mask holds 1 bit per signal, so repeated sends of a pending signal are merged into a single delivery.
- The kernel runs a handler by saving the program’s registers on its stack and redirecting it to the handler. A handler may call only async-signal-safe functions.
execresets handlers to the default action and keeps ignored signals ignored.SIGKILLandSIGSTOPcannot be blocked, ignored or handled.- A handler that runs during a blocking system call makes the call fail with
EINTR, or restart when the handler was installed withSA_RESTART.
References
signal(7),sigaction(2),sigprocmask(2),signal-safety(7)andsigreturn(2), Linux manual pages: https://man7.org/linux/man-pages/man7/signal.7.html- M. Kerrisk, The Linux Programming Interface, No Starch Press, 2010, chapters 20 to 22.
kernel/signal.candinclude/linux/sched/signal.h, Linux 5.15: https://elixir.bootlin.com/linux/v5.15/source/kernel/signal.c