Rust 系統編程指南:信號處理

信號是由操作系統或另一個進程發送給進程的軟件中斷,以通知它發生了事件。例如,當你的程序在終端上運行時,嘗試按 "control + c",它會終止進程。這是你能看到的最常見的信號和信號處理之一。接下來我們將探索如何在 Rust 中處理這個信號和其他信號。

信號可以由不同的方式觸發,如硬件、操作系統、用戶輸入或其他進程。當一個進程接收到一個信號時,這意味着一個事件已經發生,該進程可以根據信號的類型採取特定的操作。例如,進程可能需要停止運行、重新啓動或處理錯誤。

在本文中,我們將瞭解信號的用途以及如何在 Rust 編程語言中處理信號,讓我們開始吧。

信號的基礎知識

信號被認爲是事件的通知,就像我們在日常生活中對通知的反應一樣,當你接到一個事件的通知時,你要麼承擔責任,解決它,要麼選擇忽略它。類似地,操作系統信號使進程能夠對觸發的事件採取行動或什麼也不做。

例如,信號可以暫停或暫停正在運行的進程,將錯誤 (如浮點異常) 通知用戶,或提供系統警報喚醒呼叫等信息。當接收到這樣的信號時,應用程序可能需要關閉打開的句柄以釋放系統資源或終止事件可能影響的任何活動。這就是當用戶按下 "control + c" 時應用程序退出的情況。

信號類型

信號有幾種類型,有些可以處理,有些則不能。下表顯示了基於 POSIX 標準的一些信號類型及其可用代碼。這個標準是一組爲類 Unix 操作系統 (包括 Linux、macOS 和各種 Unix) 定義 api 的標準。

如下:

信號處理

信號處理是指當進程接收到特定信號時,操作系統所採取的默認操作。三種可能的信號處理是:

這意味着不是所有的信號都能被處理,應用程序只能處理操作系統允許它處理的信號。但是,還有一些其他信號,如 SIGKILL、SIGSTOP 和 SIGCONT,不能處理。例如,SIGKILL 用於強制終止進程,不能捕獲、阻塞或忽略它。

信號屏蔽

信號屏蔽是暫時阻止向進程或線程傳遞某些信號的過程。當被屏蔽時,一個信號被添加到一組阻塞的信號中,並且在解除阻塞之前不會被傳遞給進程或線程。

信號屏蔽通常用於防止在執行代碼的關鍵部分時不能被信號處理程序中斷。例如,在多線程程序中,代碼的關鍵部分可能需要在不被信號處理程序中斷的情況下原子地執行。在這種情況下,程序員可以暫時屏蔽可能中斷臨界區的信號,然後在臨界區完成後將它們解除屏蔽。

Rust 中的信號處理

現在我們已經介紹了信號的基礎知識,讓我們深入研究在 Rust 中如何處理信號!與 C 語言中內置信號處理的語言模塊不同,Rust 提供了幾個庫,使開發人員能夠輕鬆地處理信號。signal_hook、nix、libc 和 tokio 等庫主要使用 C 綁定來處理信號。

Tokio 的信號處理

讓我們看一個例子來展示如何在 Rust 中使用 tokio 處理信號。Tokio 是處理信號的完美選擇,因爲它是異步的和安全的。順便說一下,它在幕後使用的是 libc。

首先,用 Cargo 創建一個 Rust 項目:

cargo new rust-signal-handling

在 Cargo.toml 文件中加入 Tokio 依賴項:

[dependencies]
tokio = { version="1.25.0", features=["full"] }

然後,讓我們編寫一個示例代碼來處理 SIGINT 信號——當你對終端中正在運行的進程按 control + c 時觸發的信號。代碼如下:

use tokio::signal::unix::{signal, SignalKind};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut sigint = signal(SignalKind::interrupt())?;

    match sigint.recv().await {
        Some(()) => println!("Received SIGINT signal"),
        None => eprintln!("Stream terminated before receiving SIGINT signal"),
    }

    for num in 0..10000 {
        println!("{}", num)
    }

    Ok(())
}

現在,在終端上運行 cargo run 來測試代碼。當代碼運行時,按 "control +  c",你會看到如下的響應:

^CReceived SIGINT signal
0
1
2
3
4
......

在上面的代碼中,我們通過調用信號的 signalKind 方法來初始化信號的類型。SIGINT 被稱爲 interrupt(), SIGTERM 被稱爲 terminate(),你可以上面找到其他方法。在我們的例子中,我們調用 interrupt() 類型:

let mut sigint = signal(SignalKind::interrupt())?;

一旦該方法被調用,你就可以監聽該信號並使用. recv() 方法處理它,如下所示:

match sigint.recv().await {
    Some(()) => println!("Received SIGINT signal"),
    None => eprintln!("Stream terminated before receiving SIGINT signal"),
}

這基本上就是你在 Rust 中只用幾行代碼就能處理信號的方式。

Rust 中的信號屏蔽

讓我們看一個如何使用 nix 庫阻塞和解除阻塞信號的示例。對於本例,我們將使用 libc 庫。在 Cargo.toml 文件中加入 libc 依賴項:

[dependencies]
tokio = { version="1.25.0", features=["full"] }
libc = "0.2"

然後,在 src/bin/signal_mask.rs 文件中寫入如下代碼:

use libc::{sigaddset, sigemptyset, sigprocmask, SIGINT, SIG_BLOCK, SIG_UNBLOCK};
use std::thread;
use std::time::Duration;
fn main() {
    unsafe {
        // 創建一個空信號屏蔽
        let mut masklibc::sigset_t = std::mem::zeroed();
        sigemptyset(&mut mask);

        // 將SIGINT信號添加到信號屏蔽中
        sigaddset(&mut mask, SIGINT);

        // 使用信號屏蔽阻塞SIGINT信號
        sigprocmask(SIG_BLOCK, &mask as *const libc::sigset_t, std::ptr::null_mut());
    }

    println!("Blocked SIGINT signal for 5 seconds");
    thread::sleep(Duration::from_secs(5));

    unsafe {
        println!("Unblocked SIGINT signal");

        // 解除SIGINT信號的阻塞
        let mut masklibc::sigset_t = std::mem::zeroed();
        sigemptyset(&mut mask);
        sigaddset(&mut mask, SIGINT);
        sigprocmask(SIG_UNBLOCK, &mask as *const libc::sigset_t, std::ptr::null_mut());
    }
}

注意,我們將函數標記爲不安全的,我們這樣做是因爲它涉及到通過 C 標準庫的 libc 接口與操作系統信號處理機制的直接交互。正如你所看到的,我們解引用了 sigset_t 的原始指針 * const libc::sigset_t,因爲這部分代碼是不安全的。

在上面的代碼中,我們將阻塞 SIGINT 信號的傳遞,直到 5 秒之後。在這 5 秒內,如果你按下 "control + c",什麼也不會發生。然而,SIGINT 信號將在 5 秒後被觸發。

使用 "cargo run --bin signal_mask" 命令運行這段代碼並按下 "control + c",你會得到這樣的結果:

Blocked SIGINT signal for 5 seconds
^C
Unblocked SIGINT signal

在 Rust 中處理信號非常簡單,希望在這裏提供的例子對你使用 Rust 實現信號處理有所幫助。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VILPil8bFtMrPUPQt1__Lw