在 Rust 中捕获 Ctrl-C 信号的技巧

引言

本文介绍使用 libc 在 Rust 中处理 Ctrl-C 为代表的信号。是我最近从开源代码学到的技巧。

使用 libc 库访问底层操作系统接口

我们使用 libc 库中的 sigaction 函数来设置 Ctrl-C 信号的处理程序。sigaction 函数允许我们定义一个自定义的信号处理程序,并将其与特定的信号关联起来。

理解 sigaction 结构体

sigaction 函数接受一个 sigaction 结构体作为参数,该结构体定义了信号处理程序的行为。sigaction 结构体包含以下重要字段:

  • sa_sigaction: 指向信号处理程序函数的指针。

  • sa_flags: 用于配置信号处理程序的行为,如 SA_SIGINFO 标志用于指示处理程序应该接收更多的信号信息。

  • sa_mask: 在信号处理程序执行期间,哪些其他信号应该被阻塞。

在示例代码中,我们将 sa_sigaction 设置为一个名为 dummy_handler 的函数,该函数什么也不做,只是作为一个占位符。我们还将 sa_flags 设置为 SA_SIGINFO,以便在信号处理程序中获取更多的信号信息。

参考:https://man7.org/linux/man-pages/man2/sigaction.2.html

实现 Ctrl-C 信号处理

请看代码,

 1#[cfg(not(target_os = "windows"))]
 2{
 3    // Catch Ctrl-C with a dummy signal handler.
 4    unsafe {
 5        let mut new_action: libc::sigaction = core::mem::zeroed();
 6        let mut old_action: libc::sigaction = core::mem::zeroed();
 7
 8        extern "C" fn dummy_handler(
 9            _: libc::c_int, _: *const libc::siginfo_t, _: *const libc::c_void,
10        ) {
11        }
12
13        new_action.sa_sigaction = dummy_handler as libc::sighandler_t;
14        new_action.sa_flags = libc::SA_SIGINFO;
15
16        libc::sigaction(
17            libc::SIGINT,
18            &new_action as *const libc::sigaction,
19            &mut old_action as *mut libc::sigaction,
20        );
21    }
22}

解释如下:

  1. #[cfg(not(target_os = "windows"))]: 一个 Rust 属性宏,确保这段代码仅在非 Windows 操作系统上编译和执行。Windows 操作系统有自己的处理 Ctrl-C 信号的方式,我们稍后会讨论。

  2. 在大括号内的代码块中:

    • unsafe 块是因为需要直接调用 C 语言的 libc 库函数。

    • 声明了两个 libc::sigaction 结构体变量 new_actionold_action

    • 定义了一个名为 dummy_handler 的 C 语言风格的函数。这个函数仅作为一个占位符。

    • dummy_handler 函数的地址赋值给 new_action.sa_sigaction

    • new_action.sa_flags 设置为 libc::SA_SIGINFO

    • 调用 libc::sigaction 函数,将 new_action 设置为 SIGINT 信号(通常是 Ctrl-C)的新处理程序,并将旧的处理程序保存在 old_action 中。

这段代码的目的是在非 Windows 操作系统上捕获 Ctrl-C 信号,并将其交由一个空的 dummy_handler 函数处理。这样做可以防止程序在收到 Ctrl-C 信号时退出,从而允许程序在收到该信号后执行其他操作。

跨平台兼容性

在上述代码中,我们用了 #[cfg(not(target_os = "windows"))] 属性宏来确保该代码仅在非 Windows 操作系统上编译和执行。这是因为 Windows 操作系统处理 Ctrl-C 信号的方式与 Unix 系统有所不同。

在 Windows 上,我们可以用 SetConsoleCtrlHandler 函数来捕获 Ctrl-C 信号。要用 Windows 特有的 API,而不是 libc 库。具体处理如下:

 1#[cfg(target_os = "windows")]
 2{
 3    use winapi::shared::minwindef::DWORD;
 4    use winapi::um::consoleapi::SetConsoleCtrlHandler;
 5    use winapi::um::wincon::CTRL_C_EVENT;
 6
 7    extern "system" fn ctrl_c_handler(_: DWORD) -> winapi::ctypes::c_int {
 8        1 // prevent default exit behavor
 9    }
10
11    unsafe {
12        SetConsoleCtrlHandler(Some(ctrl_c_handler), 1);
13    }
14
15    ...
16}

完整代码

 1use std::thread::sleep;
 2
 3fn hello() {
 4    println!("Hello, world!");
 5    sleep(std::time::Duration::from_secs(1));
 6}
 7
 8#[cfg(not(target_os = "windows"))]
 9fn main() {
10    // Catch Ctrl-C with a dummy signal handler.
11    unsafe {
12        let mut new_action: libc::sigaction = core::mem::zeroed();
13        let mut old_action: libc::sigaction = core::mem::zeroed();
14
15        extern "C" fn dummy_handler(
16            _: libc::c_int,
17            _: *const libc::siginfo_t,
18            _: *const libc::c_void,
19        ) {
20            // Perform cleanup or other actions here
21            println!("Caught Ctrl-C signal!");
22            // Exit the program
23            std::process::exit(0);
24        }
25
26        new_action.sa_sigaction = dummy_handler as libc::sighandler_t;
27        new_action.sa_flags = libc::SA_SIGINFO;
28
29        libc::sigaction(
30            libc::SIGINT,
31            &new_action as *const libc::sigaction,
32            &mut old_action as *mut libc::sigaction,
33        );
34
35        loop {
36            hello()
37        }
38    }
39}
40
41#[cfg(target_os = "windows")]
42fn main() {
43    use winapi::shared::minwindef::DWORD;
44    use winapi::um::consoleapi::SetConsoleCtrlHandler;
45    use winapi::um::wincon::CTRL_C_EVENT;
46
47    extern "system" fn ctrl_c_handler(_: DWORD) -> winapi::ctypes::c_int {
48        // Perform cleanup or other actions here
49        println!("Caught Ctrl-C signal!");
50        // Exit the program
51        std::process::exit(0);
52        0
53    }
54
55    unsafe {
56        SetConsoleCtrlHandler(Some(ctrl_c_handler), 1);
57    }
58
59    loop {
60        hello()
61    }
62}