Course contentsShow
← Rust
Lesson 1659 of 1,91014. System Integration and EcosystemFree lesson

Signal Handling in Rust

You will handle OS signals like SIGTERM and SIGHUP in a manner compatible with Rust's runtime.

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?

  1. 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).
  2. 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.
  3. Multi-Platform: While the example above uses unix modules, 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.