【问题标题】:Rust: How to spawn child process that continues to live after parent receives SIGINT/SIGTERMRust:如何生成在父进程收到 SIGINT/SIGTERM 后继续存在的子进程
【发布时间】:2020-07-19 08:54:02
【问题描述】:

我目前正在编写一个同时启动其他应用程序的应用程序(例如firefox)。我希望这些子应用程序比父应用程序寿命更长(例如,当父应用程序退出时它们应该继续运行)。只要父进程自行退出(主进程结束,process:exit()),这就是有效的,但是如果父进程收到SIGINT(ctrl + c),SIGTERM 所有子进程都会立即死亡,因为好。我怎样才能避免这种情况? 注意:我的主要过程旨在长期存在,因此下面所有在产卵后立即退出的示例都不适合我的情况,我只是为了完整性而列出它们以显示我的内容试过了,等等

目前我只关心 Linux 支持,前提是没有干净的跨平台解决方案。

到目前为止,我已经尝试了以下方法,但没有一个让我满意:

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

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

生成的线程可能比调用者活得更久(除非调用者线程是主线程;当主线程完成时整个进程终止)。

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

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

【问题讨论】:

  • @SvetlinZarev 不幸的是没有。帖子说“这样做的传统方法是分叉两次。”但我用我的最后一个功能做到了这一点,它也不能正常工作。该帖子还说(就像我在帖子中链接的问题一样)“您需要调用setsid()”,但我不知道如何从生锈中做到这一点?
  • 正如您所说,如果您主要对 Linux 感兴趣,那么您可以使用 nix crate 调用 setsid: docs.rs/nix/0.17.0/nix/unistd/fn.setsid.html 在您的示例代码中,您从未分叉过两次。创建两个线程是不一样的。在 rust 中,当主线程退出时,所有线程都会被销毁。

标签: linux rust fork parent-child spawn


【解决方案1】:

为了防止子进程在父进程被杀死时终止,您需要进行双重分叉。这是 linux 特有的,与 rust 无关。

我正在使用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);
        }
    }
}

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

【讨论】:

  • 谢谢。这行得通。但是,当“我的”进程收到SIGINT(例如 bash 中的Ctrl+C)时,生成的子进程仍然被终止。这可以通过先在孩子中调用nix::unistd::setsid() 来避免。
【解决方案2】:

如果你想“守护”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时的流程:

  • 父母分叉孩子
  • 父级退出
  • 子调用setsid() 开始一个没有控制终端的新会话
  • 子叉孙子
  • 子退出
  • 孙子现在是守护进程

您可以查看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),
    }
}

【讨论】:

    最近更新 更多