signal-hook

Safe, structured Unix signal handling for Rust applications

Library
Cargo
v0.4.4
862stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
41/100Fair
Development Activity8
Maintenance0
Community68
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
80/100Excellent
Architecture82
Code Quality78
Innovation80
Learning Curve80

signal-hook is a Rust library that solves one of the trickiest problems in systems programming: handling Unix signals safely and correctly. Signals are process-wide, can fire on any thread, and the POSIX standard forbids most familiar operations — including locking and memory allocation — inside a signal handler. signal-hook works around these constraints by providing a global registry that lets independent parts of a program subscribe to and unsubscribe from the same signal without stepping on each other’s handlers.

Rather than forcing every consumer to write raw signal-handling code, it ships ready-made patterns for the most common cases: setting an atomic flag when a signal arrives, writing a byte to a self-pipe to wake up a poll loop, or iterating over signals as they arrive on a dedicated thread. Adapter crates extend the same primitives to Tokio, async-std, and mio for applications built on those runtimes.

What You Get

  • A global signal registry (low_level) for registering and unregistering raw signal actions across independent parts of an application.
  • The flag module for setting atomic bools/flags on signal arrival — ideal for polling loops that check for shutdown requests.
  • The iterator module’s Signals/SignalsInfo types for blocking iteration over incoming signals on a dedicated thread.
  • The low_level::pipe module for waking up poll/select-based event loops via a self-pipe when a signal fires.
  • Companion crates (signal-hook-tokio, signal-hook-async-std, signal-hook-mio) that adapt the same registry to async runtimes.

Common Use Cases

  • Graceful shutdown: catching SIGTERM/SIGINT to finish in-flight work before a daemon or server exits.
  • Config reload: watching for SIGHUP to re-read configuration files without restarting a long-running process.
  • Double-Ctrl-C handling: forcing immediate exit if a second termination signal arrives while a graceful shutdown is already underway.
  • Integrating signal delivery into async runtimes (Tokio, async-std, mio) via the companion adapter crates.

Under The Hood

Architecture The crate splits into a layered architecture around a shared backend. At the bottom sits the signal-hook-registry crate (signal-hook-registry/src/lib.rs, half_lock.rs, vec_map.rs), which owns the single OS-level sigaction() registration per signal and multiplexes an arbitrary number of callbacks onto it via a half_lock structure, guaranteeing the callbacks it invokes are async-signal-safe. signal-hook itself (src/lib.rs) re-exports this registry through low_level (src/low_level/mod.rs, pipe.rs, channel.rs, siginfo.rs, signal_details.rs) and layers three progressively higher-level, safe APIs on top: flag (src/flag.rs) for atomic-flag polling, low_level::pipe for waking poll/select loops via a self-pipe, and iterator (src/iterator/mod.rs, backend.rs, exfiltrator/*.rs) for blocking, thread-based signal consumption with pluggable Exfiltrator implementations controlling what data (bare signal number vs. extended siginfo) comes out of each delivery. The companion crates signal-hook-tokio, signal-hook-async-std, and signal-hook-mio (separate workspace members per Cargo.toml) each wrap the same iterator backend to bridge signal delivery into their respective async runtimes’ polling models, so no signal-handling logic is duplicated per runtime.

Tech Stack signal-hook is pure Rust 2018 (rust-version = 1.66 per Cargo.toml), with a minimal dependency footprint: libc (^0.2) for raw signal syscalls and its own signal-hook-registry (^1.4, path-linked within the workspace) as the only mandatory dependency. Feature flags in Cargo.toml (channel, iterator — both on by default, extended-siginfo, extended-siginfo-raw) let consumers opt out of the iterator or channel modules to shrink compiled surface area; extended-siginfo-raw pulls in an optional cc build dependency and a build.rs to compile a small C shim for extracting extended siginfo_t fields not exposed safely by libc. The repo is a Cargo workspace (Cargo.toml [workspace] members) housing five crates — the core, the registry backend, and three async-runtime adapters — versioned and released together, with dev-dependencies limited to an in-tree serial_test crate for serializing signal-touching tests.

Code Quality Testing is concentrated in three integration-test files (tests/default.rs, tests/iterator.rs at 272 lines, tests/shutdown.rs) rather than unit tests inline with the source, reflecting that signal tests must run serially and often send real signals to the test process — the serial_test dev-dependency exists specifically to prevent these tests from racing each other. The crate is warn(missing_docs)-enforced (top of src/lib.rs) and every public module carries substantial doc comments with runnable, doctested examples (e.g. the flag and iterator module docs both compile as doctests under test(attr(deny(warnings)))), a strong documentation discipline signal for a systems crate. Error handling is idiomatic std::io::Error throughout the public API rather than a custom error type, keeping the surface small; naming is consistent and the low_level:: / flag:: / iterator:: module prefixes make call sites self-documenting about which safety/abstraction tier is in use.

API Design The public API is deliberately tiered by ergonomics and risk: low_level::register offers raw, unsafe-adjacent primitives for library authors building their own abstractions, while flag::register_usize, low_level::pipe, and iterator::Signals give application authors safe, one-call entry points for the three dominant signal-handling patterns (poll a flag, wake an event loop, iterate on a thread) without needing to understand sigaction semantics. The type alias Signals = SignalsInfo<SignalOnly> keeps the common case simple while the generic SignalsInfo<Exfiltrator> parameter lets advanced users opt into origin or raw siginfo data without a separate API. Getting started requires minimal boilerplate — the README’s example is a five-line loop around Signals::new — and the crate’s chaining behavior (calling through to a pre-existing handler instead of clobbering it) is a thoughtful default that most competing signal crates don’t handle automatically.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search