信號處理

包含命令列應用程式的過程需要對作業系統發送的信號產生反應。最常見的範例可能是 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(一個可以跨執行緒分享的布林值),在熱迴圈中,或是在等待執行緒時,定期檢查它的值,當它變為真時中斷。

處理其他類型的訊號

ctrlc 函式庫只處理 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(())
}

使用 future 和串流

如果你正在使用 tokio,你很可能已經使用非同步模式和事件驅動設計在撰寫你的應用程式。與其直接使用 crossbeam 的通道,你可以在 signal-hook 中啟用 tokio-support 功能。這允許你對 signal-hook 的 Signals 類型呼叫 .into_async() 來取得一個新的類型,這個類型實作了 futures::Stream

當你在處理第一個 Ctrl+C 時收到另一個 Ctrl+C,該怎麼辦

大多數使用者會按下 Ctrl+C,然後給你的程式幾秒鐘的時間讓它離開,或告訴他們發生了什麼事。如果沒有發生,他們會再次按下 Ctrl+C。典型的行為是讓應用程式立刻離開。