Rust:如何生成在 parent 接收到 SIGINT/SIGTERM 后继续存在的 child 进程

Rust: How to spawn child process that continues to live after parent receives SIGINT/SIGTERM

我目前正在编写一个同时启动其他应用程序的应用程序(例如 firefox)。我希望这些 child 应用程序的寿命比 parent 长(例如,当 parent 退出时,它们应该继续 运行)。只要 parent 退出(main 结束,process:exit()),这就可以工作(见下面我的代码),但是如果 parent 接收到 SIGINT (ctrl + c) , SIGTERM 所有 child 进程也立即死亡。我怎样才能避免这种情况? 注意:我的主要进程是long-lived所以下面所有在产生child后立即退出的例子都不适合我的情况,我只是列出它们的完整性以显示我尝试过的内容等。

现在我只关心 Linux 支持,如果没有干净的 cross-plattform 解决方案。

到目前为止,我已经尝试了以下方法,none 的方法令我满意:

use std::{
    process::{self, Child, Command, Stdio},
    thread,
};

const EXECUTABLE: &str = "/usr/bin/firefox";

fn main() {
    // The child continues to live after our process has finished
    spawn_and_exit();

    // The child continues to live after our process has cleanly finished
    //spawn_and_continue()

    // The child gets killed as well if our process gets killed
    //spawn_and_force_shutdown()

    // Child continues to live (if our process shuts down cleanly)
    //threaded_clean_spawn()

    // Child gets killed as well
    //threaded_and_force_shutdown()

    // child gets killed as well
    //double_threaded_and_force_shutdown()
}

fn wait() {
    std::thread::sleep(std::time::Duration::from_millis(250));
}

fn hang() {
    println!("You can now kill the process (e.g. Ctrl+C)");
    loop { wait(); }
}

/// The child continues to live after our process has finished
fn spawn_and_exit() {
    println!("Spawn and exit");
    let _child = Command::new(EXECUTABLE)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    // give the process some time to actually start
    wait();
    wait();
    process::exit(0);
}


/// The child continues to live after our process has finished
fn spawn_and_continue() {
    println!("Spawn and clean shutdown");
    let _child = Command::new(EXECUTABLE)
        //.stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    // give the process some time to actually start
    wait();
}


/// The child gets killed as well if our process gets killed
fn spawn_and_force_shutdown() {
    println!("Spawn and force shutdown");
    let _child = Command::new(EXECUTABLE)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    wait();
    hang();
}


/// Child continues to live (if our process shuts down cleanly)
fn threaded_clean_spawn() {
    println!("threaded_clean_spawn");
    let _joinhandle = thread::Builder::new().spawn(|| {
        spawn_and_continue();
    });

    wait();
}


/// Child gets killed as well
fn threaded_and_force_shutdown() {
    println!("threaded_and_force_shutdown");
    let _joinhandle = thread::Builder::new().spawn(|| {
        spawn_and_continue();
    });

    hang();
}


/// child gets killed as well
fn double_threaded_and_force_shutdown() {
    println!("double_threaded_and_force_shutdown");
    let _joinhandle = thread::Builder::new().spawn(|| {
        let joinhandle = thread::Builder::new().spawn(move || {
            spawn_and_continue();
        }).unwrap();

        let _ = joinhandle.join();
        println!("inner thing returned");
    });


    hang();
}

Side-note:最初,我预计 thread::Builder::new().spawn() 会解决我的问题,因为文档 (https://doc.rust-lang.org/std/thread/struct.Builder.html#method.spawn) 指出:

The spawned thread may outlive the caller (unless the caller thread is the main thread; the whole process is terminated when the main thread finishes).

由于括号中的添加,我也尝试了double_threaded_and_force_shutdown的方法,没有成功。

这基本上是与 How to Spawn Child Processes that Don't Die with Parent? 相同的问题,但针对的是 Rust 而不是 c++。

如果您想“守护进程”,fork crate 可能会有用,这里有一个小例子:

use fork::{daemon, Fork};
use std::process::Command;

fn main() {
    if let Ok(Fork::Child) = daemon(false, false) {
        Command::new("/usr/bin/firefox")
            .output()
            .expect("failed to execute process");
    }
}

Cargo.toml 的内容:

[dependencies]
fork = "0.1"

这是调用daemon时的流程:

  • parent 分叉 child
  • parent 退出
  • child 调用 setsid() 开始一个没有控制终端的新会话
  • child分叉盛大​​child
  • child 退出
  • grandchild 现在是守护进程

您可以查看 lib.rs 代码以获得更好的想法,例如关于如何调用 setsid:

pub fn setsid() -> Result<libc::pid_t, i32> {
    let res = unsafe { libc::setsid() }; // check https://docs.rs/libc
    match res {
        -1 => Err(-1),
        res => Ok(res),
    }
}

为了防止父进程被kill时子进程也被终止,你需要double-fork。这是 linux 特有的,与生锈无关。

我正在使用 nix crate 调用 linux API(省略了正确的错误处理):

use std::{
    process::{exit, Command},
    thread::sleep,
    time::Duration,
};

use nix::{
    sys::wait::waitpid,
    unistd::{fork, ForkResult},
};

fn main() {
    match fork().expect("Failed to fork process") {
        ForkResult::Parent { child } => {
            println!("Try to kill me to check if the target process will be killed");

            // Do not forget to wait for the fork in order to prevent it from becoming a zombie!!!
            waitpid(Some(child), None).unwrap();

            // You have 120 seconds to kill the process :)
            sleep(Duration::from_secs(120));
        }

        ForkResult::Child => {
            // replace with your executable
            Command::new("/usr/bin/file-roller")
                .spawn()
                .expect("failed to spawn the target process");
            exit(0);
        }
    }
}

你一定不要忘记在第一个fork上调用waitpid,当你有它的PID时,否则它会成为一个僵尸进程。摆脱僵尸的唯一等待是调用 waitpid 以便 OS 释放任何相关资源或杀死它们的父级 - 即您的应用程序,因此只需调用 waitpid 和省去麻烦。