信号处理

像命令行应用程序这样的进程需要对操作系统发送的信号做出反应。最常见的例子可能是 Ctrl+C,这是一个通常告诉进程终止的信号。要在 Rust 程序中处理信号,您需要考虑如何接收这些信号以及如何对它们做出反应。

操作系统之间的差异

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

Windows 没有信号。您可以使用控制台处理程序来定义在事件发生时执行的回调。还有结构化异常处理,它可以处理各种类型的系统异常,例如除以零、无效访问异常、堆栈溢出等等

首先:处理 Ctrl+C

ctrlc crate 正如其名称所示:它允许您以跨平台的方式对用户按下 Ctrl+C 做出反应。使用 crate 的主要方法是这样

use std::{thread, time::Duration};

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

    // Following code does the actual work, and can be interrupted by pressing
    // Ctrl-C. As an example: Let's wait a few seconds.
    thread::sleep(Duration::from_secs(2));
}

当然,这没有太大帮助:它只打印一条消息,但没有停止程序。

在现实世界的程序中,最好在信号处理程序中设置一个变量,然后在程序中的各个位置检查该变量。例如,您可以在信号处理程序中设置一个 Arc<AtomicBool>(一个在线程之间可共享的布尔值),并在热循环中,或在等待线程时,定期检查其值,并在它变为 true 时中断。

处理其他类型的信号

ctrlc crate 只处理 Ctrl+C,或者在 Unix 系统上被称为 SIGINT(“中断”信号)的信号。要对更多的 Unix 信号做出反应,您应该查看 signal-hook。它的设计在这篇博客文章中进行了描述,并且它是目前社区支持最广泛的库。

这是一个简单的例子

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);
        }
    });

    // Following code does the actual work, and can be interrupted by pressing
    // Ctrl-C. As an example: Let's wait a few seconds.
    thread::sleep(Duration::from_secs(2));

    Ok(())
}

使用通道

您可以使用通道而不是设置变量并让程序的其他部分检查它:您创建一个通道,每当收到信号时,信号处理程序都会在该通道中发出一个值。在您的应用程序代码中,您可以使用此通道和其他通道作为线程之间的同步点。使用 crossbeam-channel,它看起来像这样

use std::time::Duration;
use crossbeam_channel::{bounded, tick, Receiver, select};
use anyhow::Result;

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(())
}

使用 futures 和 streams

如果您正在使用 tokio,您很可能已经使用异步模式和事件驱动设计来编写应用程序。您可以启用 signal-hook 的 tokio-support 功能,而不是直接使用 crossbeam 的通道。这允许您在 signal-hook 的 Signals 类型上调用 .into_async(),以获取一个实现 futures::Stream 的新类型。

当您在处理第一个 Ctrl+C 时收到另一个 Ctrl+C 时该怎么办

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