In this lesson, you will learn how to catch and respond to OS signals like SIGTERM and SIGHUP without breaking the safety guarantees of your Rust runtime.
When your Rust application runs as a background service or daemon, the operating system communicates state changes (like shutdown requests or configuration reloads) via POSIX signals. Because Rust’s memory safety model prohibits global mutable state and side effects within async handlers, you cannot simply perform complex logic inside a C-style signal handler.
Instead, the idiomatic approach is to use a "signal stream." Using crates like tokio::signal, you can map OS signals into an asynchronous stream. This allows your application to await the arrival of a signal as if it were any other asynchronous event, integrating seamlessly with your executor.
Practical Implementation
The following pattern shows how to gracefully shut down an async worker upon receiving a SIGTERM or SIGINT:
use tokio::signal::unix::{signal, SignalKind};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create signal listeners for specific OS interrupts
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
println!("Service started. Waiting for signals...");
tokio::select! {
_ = sigterm.recv() => println!("Received SIGTERM, shutting down..."),
_ = sigint.recv() => println!("Received SIGINT, shutting down..."),
};
// Perform cleanup logic here: closing DB pools, flushing buffers, etc.
Ok(())
}
Why this approach?
- Runtime Compatibility: By using
tokio::select!, you ensure that signal handling happens within the event loop, avoiding the dangers of executing code in the restricted context of a kernel-triggered signal handler (which can lead to deadlocks if it tries to acquire a mutex). - Deterministic Shutdown: This method treats the signal as a first-class asynchronous message. It allows your task to complete its current work, commit transactions, or drain queues before exiting, rather than the process being killed abruptly.
- Multi-Platform: While the example above uses
unixmodules, abstractions provided by common async runtimes allow you to gate platform-specific code using conditional feature flags to maintain cross-platform compatibility.
Key Takeaway: Never execute complex logic inside raw signal handlers; instead, convert OS signals into asynchronous streams to handle them gracefully within your runtime's event loop.