Library / SDK
vorner/signal-hook avatar
vorner/signal-hook

signal-hook: one Unix signal, many Rust handlers

Rust library allowing to register multiple handlers for the same signal

878 stars95 forksRustApache-2.0

At a glance

What is it?
signal-hook is a Rust crate that keeps a global registry of Unix signal actions, so several parts of a program can react to the same signal and later release their handlers instead of fighting over one global slot. It adds a small set of safe helper actions and ships async integrations in the same workspace.
Who is it for?
Adopt signal-hook if your Rust program has more than one component that needs SIGTERM, SIGINT or SIGHUP, because a global registry with removable actions is the only clean way to share those signals without a hand-rolled dispatcher. Skip it if you need exactly one handler, since libc plus a single closure does less work.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 20 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Signals are global, so a global registry is the only workable shape

Unix signals are a process-wide resource, and that single fact drives the whole design of this crate. The README lays the problem out in three parts. Because a signal is global, a library that installs its own handler risks disturbing another library in the same process. Because a handler can run on whatever thread happens to receive the signal, it needs synchronization. And because a signal can interrupt a thread at any moment, naive handling becomes race prone. There is a fourth constraint that bites hardest: the POSIX standard restricts what a signal handler may call to a very small set of functions. Mutexes, other locking mechanisms, and memory allocation and deallocation are not on that list.

That last point is why signal handling in Rust tends to be awkward. The idiomatic Rust answer to shared state is a Mutex or an Arc, and a signal handler is not allowed to take a mutex lock. The README describes the project as a library for safe and correct Unix signal handling in Rust, and the answer it gives is a global registry of actions performed on arrival of signals, plus several helper actions for the common cases, available from safe Rust.

The mechanism behind that registry is global by design, matching the global nature of the resource it manages. Multiple actions can be registered for the same signal, and those actions can be removed later on. That last property is what separates this crate from the usual pattern of a single handler that dispatches into a channel, and it is the property that matters most when a program has more than one subsystem with an interest in being told about termination.

Chaining the previous handler fixes one leak and keeps another

There is a common technique for cooperating with a handler that was installed before yours. You install your own handler and, from inside it, call the previous handler. The README describes this and its limitation precisely: if there was a previous signal handler when the first action for a signal is registered, it is chained, but the original one can't be removed.

So the chain is a one-way ratchet. Chaining avoids the collision, so the other library keeps working, and the cost is that once your first action for a signal is registered, whatever handler preceded it becomes part of the call path for the life of the process. The README is blunt about the reason: chaining makes it impossible to remove the old signal handlers from the chains in any practical manner. If a library earlier in the process installed a handler, that handler stays reachable even after this crate releases every action it registered.

The registry solves the mirror-image problem. Actions that this crate registered can be removed later on, which is what you want in tests, in reloadable subsystems, and in code that hands a signal handler over and later takes it back. The asymmetry is worth holding onto when you evaluate the crate: your own registrations are revocable, and anyone else's are not. A program that mixes several signal-aware libraries should expect only partial cleanup, and the README does not document a way to unwind a chained handler.

The flag helper turns SIGTERM into a polled boolean

The crate's most-used path is the flag module, and the README's example is explicitly more of a tour of what is possible than a template for each application. It registers a handler for SIGTERM that sets an atomic boolean, then spins in a loop until that boolean flips. The loop body carries a warning that matters for real programs: if the loop could block forever, then there's no guarantee the signal will have any effect.

That caveat is the practical constraint of the whole approach. A signal that arrives while the main thread is blocked in an unrelated call may not be observed until that call returns, and a process that never polls the flag will never notice the termination request. The library gives you a safe way to set a flag from a handler context; the responsibility for polling it stays in your program.

The example is short enough to read as a whole, which is a good sign for a crate you might drop into a service:

rust
use std::io::Error;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

fn main() -> Result<(), Error> {
    let term = Arc::new(AtomicBool::new(false));
    signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&term))?;
    while !term.load(Ordering::Relaxed) {
        // Do some time-limited stuff here
        // (if this could block forever, then there's no guarantee the signal will have any
        // effect).
    }
    Ok(())
}

Note what the handler does not do. It does not lock, it does not allocate, and it does not call back into application code that might do either. It flips an atomic, and the program reads that atomic on its own schedule.

Channel and iterator features decide how signals reach your code

Beyond raw registration, the crate offers helper actions for the common cases, available from safe Rust. Which ones you get is decided by Cargo features rather than by a runtime setting, and the default set is not minimal. The manifest defines the feature graph explicitly:

toml
[features]
channel = []
default = ["channel", "iterator"]
iterator = ["channel"]

A default build therefore already includes both a channel-based path and an iterator-based path, and the iterator feature depends on the channel feature rather than standing alone. The manifest also carries a TODO comment about unifying them on the next breaking release, which tells you something useful: these two delivery models are still separate in version 0.4.5 and are expected to be reconciled at some later breaking change. If you write code against the iterator path, a future major version may change its shape.

The extended signal information features, extended-siginfo and extended-siginfo-raw, pull in the channel and iterator paths plus a raw variant that turns on the optional cc build dependency. The cc dependency is marked optional and sits in the build-dependencies table, so the raw route compiles C code at build time. Documentation builds on docs.rs enable all features and pass the docsrs cfg, so the generated API reference covers the raw path that a normal build may not include. If you need the raw signal information, expect a build step rather than a pure Rust dependency.

The workspace already carries three async integrations and a serial test crate

The project directory is not a single crate but a workspace with six members, and that layout explains why the crate claims to cover more than plain blocking Rust. The member list names the root crate, the low-level registry crate, three async runtime integrations, and a test helper:

toml
[workspace]
members = [
    "./",
    "signal-hook-registry",
    "signal-hook-tokio",
    "signal-hook-mio",
    "signal-hook-async-std",
    "serial_test",
]

The presence of signal-hook-tokio, signal-hook-mio and signal-hook-async-std in the same workspace is the strongest practical signal that this project has been thought about in async contexts, where the polling problem described earlier is acute. A task parked on a future is not going to notice a flag set by a signal handler, so those integrations have to deliver the event into the runtime's own primitive instead.

The serial_test member is the giveaway that signal tests interfere with each other: a test that installs a global handler changes process-wide state for every other test running at the same time, so those tests have to be run one after another. The top-level directory also holds a CHANGELOG.md, a build.rs, a rustfmt.toml, an examples directory with a print.rs file, a tests directory, and two lockfiles, Cargo.lock alongside Cargo.lock.old.

Adding it to a Cargo project

The README gives no shell installation recipe, so there is no command to copy here. The crate documents itself on docs.rs, and the manifest shows the dependency shape a project would use, with the low-level registry crate pulled in alongside the main one:

toml
[dependencies]
libc = "^0.2"
signal-hook-registry = { version = "^1.4", path = "signal-hook-registry" }

Three things are visible in that block. The version is a caret range on 0.2 for libc, the usual way Rust packages refer to the platform bindings. The registry crate is a path dependency here because the snippet comes from the workspace root, where the sibling directory is available; in a separate project you would take the published version instead. And the registry crate sits at version 1.4 while the main crate is at 0.4.5, so the two carry independent version lines.

The manifest also pins the toolchain floor at rust-version 1.66 and the edition to 2018, and the package version itself is 0.4.5. Do not rely on the maintenance badge in the manifest to tell you whether the project is alive. The repository is not archived, and the last push was on 2026-10-04, but GitHub shows no releases for this repository, so version bumps reach users through the registry and the changelog file rather than through tagged releases.

The alternative is the lower-level registry crate, or libc itself

The obvious alternative for a Rust program that needs exactly one handler is to call libc directly. The libc crate is already a dependency of this project, so it is present in any build that pulls signal-hook in. The tradeoff is direct: a raw handler cannot take a lock, cannot allocate, and cannot return a value, so the code that runs inside it has to be small and self-contained, and the state it touches has to be a plain atomic. You also own the chaining behaviour and the removal behaviour, because with libc there is no registry to remove anything from, and every registration is a process-wide side effect that your tests must not race over.

The middle option sits inside this same project. signal-hook-registry appears as a dependency of the main crate at version 1.4 and is a workspace member of its own, which is where the low-level plumbing lives. The root crate builds the safe API and the helper actions on top of that base. If you want the global registry and the removable actions but not the helper set, the lower-level crate is the one to look at. If you want the helpers and the async integrations, use the root crate.

For an async program, the workspace's own integrations are the third option, and they exist because the blocking example's warning about a signal having no effect applies even more strongly to a task that is parked on a future. A project that already uses tokio, mio or async-std can stay inside that runtime's own primitive rather than polling an atomic from a blocking loop.

Licence, versioning, and what the changelog has to carry

The project is dual licensed. The README states it is licensed under either the Apache License, Version 2.0, or the MIT license, at your option, with LICENSE-APACHE and LICENSE-MIT files in the top-level directory. The manifest records the same choice as license = "MIT OR Apache-2.0", so a downstream project can pick the terms it prefers without asking. The contribution section adds that any contribution is dual licensed the same way unless the contributor states otherwise.

For dependency hygiene, the useful facts are narrow. The crate version is 0.4.5, the minimum supported Rust version is 1.66, and the edition is 2018, so a project on a current toolchain can consume it without ceremony, while a project pinned to an older compiler needs to check that floor first. The public API carries a zero major version, which means upstream is free to make breaking changes, and the manifest's own TODO about unifying the channel and iterator features on the next breaking release is a concrete hint of what one of those might involve. Read the CHANGELOG.md before a version bump, since there are no tagged GitHub releases to skim.

The repository is not archived and the last push was on 2026-10-04, so the work is not stale. What that does not tell you is the release cadence. GitHub shows no releases for this project, and the README does not document a support policy or a deprecation timeline, so an upgrade means reading the changelog file yourself.

Editorial conclusion

Adopt signal-hook if your Rust program has more than one component that needs SIGTERM, SIGINT or SIGHUP, because a global registry with removable actions is the only clean way to share those signals without a hand-rolled dispatcher. Skip it if you need exactly one handler, since libc plus a single closure does less work. Before committing, check the shutdown path your program depends on, because the README notes that a loop which can block forever gives no guarantee the signal will have any effect, so your main loop has to poll something the signal can set.

Frequently asked questions

What does signal-hook do for Rust programs?

It provides a global registry of actions performed on arrival of Unix signals. Multiple actions can be registered for the same signal, and those actions can be removed later on, which the README presents as the answer to the problem of global handler collisions between libraries.

How do I shut down a Rust program on SIGTERM with signal-hook?

Use the flag helper to register an action for signal_hook::consts::SIGTERM that sets an atomic boolean, then poll that boolean in your main loop. The README example warns that if the loop could block forever, then there is no guarantee the signal will have any effect, so the loop has to run periodically.

Can signal-hook cooperate with a handler another library installed first?

Yes, but only in one direction. If there was a previous signal handler when the first action for a signal is registered, it is chained, and the README notes the original one can't be removed. Handlers this crate registered can be removed, but a chained earlier handler stays in the call path.

Which Rust version and licence does signal-hook target?

The manifest declares rust-version 1.66 with edition 2018, and the crate version is 0.4.5. The licence is dual, recorded as MIT OR Apache-2.0, with LICENSE-APACHE and LICENSE-MIT files in the project directory.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. vorner/signal-hook on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vorner-signal-hook.svg)](https://hysenlabs.com/projects/vorner-signal-hook)

Was this useful?