Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Klaxon, an owl, is the night watch. Signal flags numbered 2, 9 and 15 fly from a mast, a bell rings, and a hare that was asleep jumps awake.

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 SIGKILL cannot be blocked, ignored or handled
  • EINTR and SA_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.

NumberNameSent whenDefault action
2SIGINTCtrl-C is pressed on the terminalend the process
3SIGQUITCtrl-\ is pressedend the process and write a core dump
9SIGKILLa program calls kill with itend the process; cannot be changed
11SIGSEGVthe process accesses memory it has no mapping for (chapter 7)end the process and write a core dump
13SIGPIPEthe process writes to a pipe or socket with no reader (chapter 1)end the process
15SIGTERMa program calls kill; the default of the kill commandend the process
17SIGCHLDa child of the process ends or stops (chapter 3)ignore
19SIGSTOPa program calls kill with itstop the process; cannot be changed
20SIGTSTPCtrl-Z is pressedstop the process
18SIGCONTfg, bg, or killcontinue 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.

  1. The terminal passes the byte 0x03 to the kernel’s terminal driver.
  2. The driver recognises it as the interrupt character and does not put it in the input of the program. It sends SIGINT to every process of the terminal’s foreground process group, the set of processes the shell started for the current command line.
  3. Sending means setting a bit: the kernel sets the bit for SIGINT in the pending mask of each of those processes. Nothing else has happened to them yet.
  4. A process that is sleeping in a system call is woken so that it can notice.
  5. 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.
What happens when Ctrl-C is pressed. The terminal receives the byte 0x03. The terminal driver in the kernel recognises the interrupt character and sends SIGINT to each process of the foreground process group. The pending bit of SIGINT is set in the mask of each process. A process that sleeps in a system call is woken. When the process returns to user mode, the kernel performs the action of the signal.
Figure 5.1 Ctrl-C, from the keyboard to the action of 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.

StateContentsChanged by
Pending mask1 bit per signal: the signal has been sent and not yet deliveredkill sets a bit; delivery clears it
Blocked mask1 bit per signal: delivery of the signal is postponedsigprocmask
Actionper signal: the default, ignore, or the address of a handler functionsigaction

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.

A sender calls kill with a pid and SIGUSR1. The kernel sets bit 10 in the pending mask of the target. The target also has a blocked mask, here with bit 2 set. When the target is about to return to user mode, each signal that is pending and not blocked is delivered, and its action is performed: the default, which ends the process for most signals; ignore, which discards it; or a handler function.
Figure 5.2 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 */
        /* ... */
};
FieldMeaning
sigset_tA set of signals as a bit mask of 64 bits. Signal n is bit n - 1.
sigpending.signalThe pending mask.
sighand_struct.actionThe table of actions, indexed by signal number.
sa_handlerThe action: the constant SIG_DFL for the default, SIG_IGN for ignore, or the address of a handler.
sa_flags, sa_maskOptions for a handler, set by the program with sigaction.
task_struct.blockedThe blocked mask. Each thread has its own.
task_struct.pending and signal_struct.shared_pendingThere 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 structures of signal handling. Each thread has a struct task_struct with a blocked mask and a pending set for signals sent to that thread. Each process has a struct signal_struct with shared_pending for signals sent to the process. A struct sighand_struct holds the array action with a struct k_sigaction for each of the 64 signals: sa_handler, sa_flags and sa_mask.
Figure 5.3 The structures that hold the pending signals, the blocked mask and the actions.

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.

  1. Drop the leading zeros: 0x10200.
  2. Write each hexadecimal digit as 4 bits: 1 0000 0010 0000 0000.
  3. Number the bits from the right, starting at 0. The bits that are 1 are bit 9 and bit 16.
  4. Add 1 to each: signals 10 and 17, SIGUSR1 and SIGCHLD.
The mask 0x10200 written in binary, with bit 0 on the right. Bits 9 and 16 are 1 and all others are 0. Bit 9 stands for signal 10, SIGUSR1, and bit 16 for signal 17, SIGCHLD. Bit n stands for signal n plus 1.
Figure 5.4 The mask 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. exec replaces the program, so a handled signal returns to the default action. Ignore and default are independent of the program and are kept.
  • SIGKILL and SIGSTOP are 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.

  1. The process is in the kernel, about to return to user mode, with its user-mode registers saved.
  2. The kernel copies those saved registers to the user stack of the process, in a signal frame.
  3. 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.
  4. It returns to user mode. The process now executes the handler, as if the handler had been called at the point of interruption.
  5. When the handler returns, sigreturn enters the kernel, which restores the registers from the signal frame. The program continues at the instruction where it was interrupted.
How a handler is run. The program is interrupted at any instruction. The kernel saves the registers in a frame on the user stack and sets the program counter to the address of the handler. The handler runs in user mode on the same stack. When it returns, it returns into a call of sigreturn, and the kernel restores the registers so that the program continues where it was interrupted.
Figure 5.5 The kernel runs a handler by rewriting the state the program returns to.

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
OutputConclusion
pending mask 0x200 after 3 sends0x200 is bit 9, signal 10, SIGUSR1. The mask records that the signal is pending and holds no count.
the handler ran 1 time3 sends were merged into a single delivery.
SIGINT ignored, SIGUSR1 caught, SIGTERM not ignoredEach signal has its own entry in the action table.
after exec: SIGTERM ignored, SIGUSR1 not caughtIgnore survives exec. A handler is reset to the default.
killed by signal 9SIGTERM 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 processKernel fieldMeaning
pendingshared_pending.signalmask of 64 bits; signal n is bit n - 1
blockedtask_struct.blockedmask of 64 bits
actions, restartsighand->action[n]default, ignore or handle; the SA_RESTART flag
handled, killedByhow many handlers have run; the signal that ended the process
call, outcomethe system call the process is blocked in; what a signal did to it
OperationSystem callBehaviour
setAction(signal, action, restart)sigactioninstalls an action; fails with EINVAL for SIGKILL
block(mask), unblock(mask)sigprocmaskchanges the blocked mask; SIGKILL cannot be added
send(signal)killdiscards 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.

A process calls read and sleeps because there is no data. A signal arrives, and the kernel wakes the process and abandons the read. The handler runs in user mode. Then, for a handler installed without SA_RESTART, read returns -1 with the error EINTR. For a handler installed with SA_RESTART, the kernel calls read again.
Figure 5.6 A handled signal interrupts a blocking call.

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

CommandShows
kill -lthe names and numbers of all signals
grep Sig /proc/<pid>/statusthe 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.
  • exec resets handlers to the default action and keeps ignored signals ignored.
  • SIGKILL and SIGSTOP cannot 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 with SA_RESTART.

References