Course contentsShow
← Rust
Lesson 1264 of 1,91011. Asynchronous ProgrammingFree lesson

Async Signal Handling

Write interrupt-safe code that responds to OS signals while maintaining executor integrity.

In this lesson, you will learn how to bridge the gap between low-level OS interrupts and your high-level asynchronous executor without corrupting your runtime’s state.

The Challenge of Signals

When a process receives an OS signal (like SIGINT or SIGTERM), the OS interrupts your thread to run a signal handler. Because signal handlers execute in a restricted environment, you cannot safely perform most actions—such as allocating memory or acquiring a standard std::sync::Mutex—because these operations are not re-entrant. If your executor happens to be in the middle of one of these operations when a signal arrives, you risk a deadlock or memory corruption.

The Async Strategy: Channel-Based Decoupling

To handle signals safely in an async context, you must treat the signal handler as a thin "bridge" that pushes an event into a safe queue. The actual processing should happen inside your async executor, far away from the interrupt context.

In Rust, we typically use an async-signal crate or tokio::signal to perform this "bridge" operation. These tools set up a Unix signal handler that simply writes a single byte into a pipe or channel. Your async task, which is already safely running on the executor, waits for that signal by asynchronously reading from that pipe.

use tokio::signal::unix::{signal, SignalKind};

async fn run_service() {
    let mut stream = signal(SignalKind::interrupt()).unwrap();

    loop {
        tokio::select! {
            _ = stream.recv() => {
                println!("Shutdown signal received safely!");
                break;
            }
            // Other application logic here
            _ = do_work() => {}
        }
    }
}

Maintaining Integrity

By using tokio::select! or similar stream-based patterns, you ensure that the response to the signal is treated as just another "future" in your task graph. This maintains executor integrity because the heavy lifting of cleanup (like closing database handles or flushing logs) occurs within the controlled, non-interrupt environment of your async tasks. You never run logic directly inside the OS signal handler; you only signal the runtime to "wake up" the task listening for that notification.

Key Takeaway: Never execute complex logic inside an OS signal handler; instead, use a non-blocking channel to relay the signal to an async task that handles the cleanup safely within the executor's scope.