Signals & Process Control
Handle OS signals and manage child processes in long-running Rust services and CLIs.
Recipe
use tokio::signal;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tokio::select! {
_ = signal::ctrl_c() => {
eprintln!("shutting down gracefully");
}
res = run_server() => res?,
}
Ok(())
}When to reach for this: Servers, workers, and CLIs that must shut down cleanly on Ctrl+C or SIGTERM.
Working Example
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::signal;
use tokio::time::{sleep, Duration};
struct Supervisor {
shutdown: Arc<AtomicBool>,
child: Option<Child>,
}
impl Supervisor {
fn spawn_worker(&mut self) -> anyhow::Result<()> {
let child = Command::new("worker-bin")
.stdout(Stdio::piped())
.spawn()?;
self.child = Some(child);
Ok(())
}
fn stop_worker(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let shutdown = Arc::new(AtomicBool::new(false));
let mut sup = Supervisor { shutdown: shutdown.clone(), child: None };
sup.spawn_worker()?;
tokio::select! {
_ = signal::ctrl_c() => {
shutdown.store(true, Ordering::SeqCst);
sup.stop_worker();
}
_ = async {
while !shutdown.load(Ordering::SeqCst) {
sleep(Duration::from_secs(1)).await;
}
} => {}
}
Ok(())
}What this demonstrates:
ctrl_c()integrates with Tokio's async runtimeCommand::spawnlaunches child processeskill+waitprevents zombie processes- Atomic flag coordinates shutdown across tasks
Deep Dive
Unix Signals
| Signal | Typical use |
|---|---|
| SIGINT | Ctrl+C from terminal |
| SIGTERM | Container orchestrator stop |
| SIGHUP | Reload config (daemons) |
On Unix, use tokio::signal::unix::signal for finer control.
Process Groups
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
libc::setpgid(0, 0);
Ok(())
});
}Put children in their own process group to signal them as a unit.
Gotchas
- Async-unsafe signal handlers - Only async-signal-safe ops in raw handlers. Fix: Set a flag; handle in main thread.
- Zombie children - Forgetting
waitleaks PIDs. Fix: Always reap children after they exit. - Kill vs graceful stop -
killmay corrupt child state. Fix: Send SIGTERM first; escalate to SIGKILL after timeout. - Windows signal differences - No POSIX signals. Fix: Use
tokio::signal::ctrl_candctrl_breakon Windows. - Double shutdown - Ctrl+C during cleanup panics. Fix: Use
AtomicBoolguard and idempotent shutdown logic.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
signal-hook crate | Sync apps without Tokio | Already on Tokio runtime |
| systemd notify | Service readiness protocol | Simple CLI tools |
| Process supervisor (systemd, k8s) | Production fleet management | Local dev scripts |
FAQs
Graceful shutdown pattern?
Stop accepting work, drain in-flight requests, flush logs, then exit 0.
How long to wait before SIGKILL?
30 seconds is common for HTTP servers. Document your timeout in ops runbooks.
Reload config on SIGHUP?
Re-read config file and swap Arc<Config> atomically. Do not restart listeners blindly.
Child stdout streaming?
Read child.stdout in a dedicated thread or tokio::process async task.
Exit code propagation?
Map child exit code to parent: std::process::exit(status.code().unwrap_or(1)).
Orphan processes?
Use process groups or prctl on Linux to kill children when parent dies.
Testing signal handling?
Send signals in integration tests with nix::sys::signal::kill on the test process child.
PID files?
Write PID to /var/run/app.pid for init scripts. Remove on shutdown.
Daemonizing?
Prefer systemd or container entrypoints over classic double-fork daemons.
Resource limits?
Set ulimit via prlimit on Linux before spawning heavy worker children.
Related
- Systems Basics -
Commandfundamentals - libc & Syscalls - raw signal APIs
- Cross-Platform Systems Code - Windows vs Unix
- Building Sound Bindings - safe shutdown patterns
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.