Environment and Process Management

Process Termination

Process termination stops a running process—either the current process via main return, std::process::exit, or std::process::abort, or a spawned child process via Child::kill. Each termination path makes a critical trade-off between running RAII destructors (Drop) and exiting immediately at the operating system level.

Termination Paths for Current Process:

1. Return from `fn main()`
   [Unwind main stack] -> [Run all Drop impls] -> [Flush stdio] -> [OS exit]

2. `std::process::exit(code)`
   [Skip all stack Drop impls!] -> [Flush stdout/stderr] -> [OS exit(code)]

3. `std::process::abort()`
   [Skip all Drop impls!] -> [Skip stdio flush!] -> [Raise SIGABRT / crash]

Comparing Current-Process Termination Mechanisms

Mechanism Return Type Runs Stack Drops? Flushes std::io::stdout? Background Threads
Returning from main() () / ExitCode / Result Yes Yes Terminated when main exits
std::process::exit(code) ! (never returns) No Yes (C runtime streams) Terminated immediately without unwinding
std::process::abort() ! (never returns) No No Terminated immediately via crash signal

Why std::process::exit Skips Destructors

std::process::exit(code: i32) -> ! hands control directly to the operating system's C runtime exit routine. Any local variables currently alive on the call stack—such as BufWriter<File>, temporary file guards, or database transaction handles—are never dropped.

struct CleanupGuard(&'static str);

impl Drop for CleanupGuard {
    fn drop(&mut self) {
        println!("Dropped guard: {}", self.0);
    }
}

fn run_app() -> i32 {
    let _guard = CleanupGuard("database-connection");
    println!("Performing work inside run_app()");
    0
}

fn main() {
    // Calling `run_app()` and letting its stack frame return ensures `_guard` is dropped
    // BEFORE `process::exit` is ever called.
    let code = run_app();
    println!("Exiting with code {code}");
    std::process::exit(code);
}
Performing work inside run_app()
Dropped guard: database-connection
Exiting with code 0

Whenever possible, return std::process::ExitCode from fn main() instead of calling std::process::exit(). If you must call std::process::exit(), isolate all work and RAII resources inside a helper function that returns an exit code to main(), and call process::exit() only as the final line of main().

Terminating a Child Process with Child::kill()

For a spawned Child process, child.kill() requests immediate termination (SIGKILL on Unix, TerminateProcess on Windows). Calling kill() sends the termination signal but does not reap the process entry from the operating system table; you must still call child.wait() afterward.

use std::io;
use std::process::{Command, Stdio};

fn main() -> io::Result<()> {
    let mut child = Command::new("rustc")
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;

    // Send termination signal
    child.kill()?;

    // Reap the zombie process entry from the OS process table
    let status = child.wait()?;
    println!("Child terminated and reaped; success = {}", status.success());
    Ok(())
}
Child terminated and reaped; success = false