01-信号处理

在 Rust CLI 中处理信号

译文 · 基于 Command Line Applications in Rust

信号处理

原文链接: https://rust-cli.github.io/book/in-depth/signals.html

像命令行应用这样的进程,需要响应操作系统发来的信号。最常见的例子大概是 Ctrl+C,这个信号通常表示让进程终止。在 Rust 程序中处理信号时,你需要考虑如何接收这些信号,以及如何对它们做出反应。

操作系统之间的差异

在 Unix 系统(如 Linux、macOS 和 FreeBSD)上,进程可以接收信号。它可以按默认(操作系统提供的)方式响应,捕获信号并以程序自定义的方式处理,或者完全忽略该信号。

Windows 没有信号。你可以使用 Console Handlers 定义在事件发生时执行的回调。此外还有 结构化异常处理,用于处理各种系统异常,例如除以零、无效访问异常、栈溢出等。

首先:处理 Ctrl+C

ctrlc crate 正如其名:它允许你以跨平台的方式响应用户按下 Ctrl+C。使用该 crate 的主要方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::{thread, time::Duration};

fn main() {
    ctrlc::set_handler(move || {
        println!("received Ctrl+C!");
    })
    .expect("Error setting Ctrl-C handler");

    // 下面的代码执行实际工作,可被 Ctrl-C 打断。
    // 举例:等待几秒钟。
    thread::sleep(Duration::from_secs(2));
}

这当然没什么用:它只是打印一条消息,并不会真正停止程序。

在真实程序中,更好的做法是在信号处理函数里设置一个变量,然后在程序各处检查它。例如,你可以在信号处理函数中设置一个 Arc<AtomicBool>(可在线程间共享的布尔值),并在热循环中,或在等待某个线程时,定期检查其值,当它变为 true 时跳出。

处理其它类型的信号

ctrlc crate 只处理 Ctrl+C,也就是 Unix 系统上称为 SIGINT(「中断」信号)的情况。若要响应更多 Unix 信号,应看看 signal-hook。其设计在这篇博文中有说明,目前它是社区支持最广的相关库。

下面是一个简单例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use signal_hook::{consts::SIGINT, iterator::Signals};
use std::{error::Error, thread, time::Duration};

fn main() -> Result<(), Box<dyn Error>> {
    let mut signals = Signals::new([SIGINT])?;

    thread::spawn(move || {
        for sig in signals.forever() {
            println!("Received signal {:?}", sig);
        }
    });

    // 下面的代码执行实际工作,可被 Ctrl-C 打断。
    // 举例:等待几秒钟。
    thread::sleep(Duration::from_secs(2));

    Ok(())
}

使用 channel

除了设置变量并让程序其它部分去检查之外,你还可以使用 channel:创建一个 channel,每当收到信号时,由信号处理函数向其中发送一个值。在应用代码中,你把这个 channel 和其它 channel 一起用作线程之间的同步点。用 crossbeam-channel 大致可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use anyhow::Result;
use crossbeam_channel::{Receiver, bounded, select, tick};
use std::time::Duration;

fn ctrl_channel() -> Result<Receiver<()>, ctrlc::Error> {
    let (sender, receiver) = bounded(100);
    ctrlc::set_handler(move || {
        let _ = sender.send(());
    })?;

    Ok(receiver)
}

fn main() -> Result<()> {
    let ctrl_c_events = ctrl_channel()?;
    let ticks = tick(Duration::from_secs(1));

    loop {
        select! {
            recv(ticks) -> _ => {
                println!("working!");
            }
            recv(ctrl_c_events) -> _ => {
                println!();
                println!("Goodbye!");
                break;
            }
        }
    }

    Ok(())
}

使用 future 与 stream

如果你在使用 tokio,那么很可能已经在用异步模式和事件驱动设计来编写应用。与其直接使用 crossbeam 的 channel,你可以启用 signal-hook 的 tokio-support 特性。这样就可以在 signal-hook 的 Signals 类型上调用 .into_async(),得到一个实现了 futures::Stream 的新类型。

正在处理第一次 Ctrl+C 时又收到一次 Ctrl+C 该怎么办

大多数用户会按下 Ctrl+C,然后给你的程序几秒钟时间退出,或者告诉他们发生了什么。如果没有动静,他们会再按一次 Ctrl+C。典型行为是让应用立即退出。

最后修改 August 11, 2026: 更新 (70a5af133)