文盤Rust -- 把程式作為守護行程啟動

2022-11-02 12:00:29

當我們寫完一個伺服器端程式,需要上線部署的時候,或多或少都會和作業系統的守護行程打交道,畢竟誰也不希望shell關閉既停服。今天我們就來聊聊這個事兒。

最早大家部署應用的通常操作是 「nohup xxxx &」,別說像weblogic 或者其他java 容器有啟動指令碼,裡面其實也差不多;很喜歡 nginx的 -d 引數,或者像redis 組態檔裡可以指定是否以守護行程啟動。看起來很優雅。

那麼,使用rust 寫一個伺服器端程式能不能優雅的使用一個引數指定應用 daemon 模式啟動,同時使用stop 方式優雅的停機呢?我們通過一個例子來說說基本的實現方式。

範例程式碼依然整合在[interactcli-rs](https://github.com/jiashiwen/interactcli-rs)工程中。

首先來模擬一個啟動的服務程序 /src/server/server.rs

pub fn start(prefix: String) {
    for i in 0..1000 {
        println!("{}", prefix.clone() + &i.to_string());
        thread::sleep(Duration::from_secs(1));
    }
}

 

 程式每秒輸出一個字串,持續999秒,這個時間足夠驗證實驗結果了。

後臺啟動有兩個實現,分別是利用[fork](github.com/immortal/fork) 或 [daemonize](github.com/knsd/daemonize),這兩個crate 實現原理類似,但在使用上稍有不同。

/src/cmd/cmdserver.rs,構建了兩個啟動子命令,分別來呼叫 fork 和 daemonize的守護行程啟動實現.

 

pub fn new_server_cmd() -> Command {
    clap::Command::new("server")
        .about("server")
        .subcommand(server_start_byfork())
        .subcommand(server_start_bydaemonize())
}

pub fn server_start_byfork() -> Command {
    clap::Command::new("byfork")
        .about("start daemon by fork crate")
        .arg(
            Arg::new("daemon")
                .short('d')
                .long("daemon")
                .action(ArgAction::SetTrue)
                .help("start as daemon")
                .required(false),
        )
}
pub fn server_start_bydaemonize() -> Command {
    clap::Command::new("bydaemonize")
        .about("start daemon by daemonize crate")
        .arg(
            Arg::new("daemon")
                .short('d')
                .long("daemon")
                .action(ArgAction::SetTrue)
                .help("start as daemon")
                .required(false),
        )
}

 

 server 的子命令 byfork 啟動 通過 fork 實現的功能,bydaemonize 則呼叫通過 daemonize 的功能實現。

命令解析的程式碼在 /src/cmd/rootcmd.rs 檔案中。

先來看看基於 fork 的實現: 

if let Some(startbyfork) = server.subcommand_matches("byfork") {
    println!("start by fork");
    if startbyfork.get_flag("daemon") {
        let args: Vec<String> = env::args().collect();
        if let Ok(Fork::Child) = daemon(true, false) {
            // 啟動子程序
            let mut cmd = Command::new(&args[0])
            for idx in 1..args.len() {
                let arg = args.get(idx).expect("get cmd arg error!");
                // 去除後臺啟動引數,避免重複啟動
                if arg.eq("-d") || arg.eq("-daemon") {
                    continue;
                }
                cmd.arg(arg);
            
            let child = cmd.spawn().expect("Child process failed to start.");
            fs::write("pid", child.id().to_string()).unwrap();
            println!("process id is:{}", std::process::id());
            println!("child id is:{}", child.id());
        }
        println!("{}", "daemon mod");
        process::exit(0);
    }
    start("by_fork:".to_string());
}

 

首先,通過 Fork::daemon 函數派生出一個子程序;然後解析一下當前命令,去掉 -d 引數,構建一個啟動命令,子命令啟動,退出父程序。這基本符合作業系統建立守護行程的過程 -- 兩次 fork。

再來看看基於 daemonize 的實現:

 

if let Some(startbydaemonize) = server.subcommand_matches("bydaemonize") {
            println!("start by daemonize");
            let base_dir = env::current_dir().unwrap();
            if startbydaemonize.get_flag("daemon") {
                let stdout = File::create("/tmp/daemon.out").unwrap();
                let stderr = File::create("/tmp/daemon.err").unwrap();

                println!("{:?}", base_dir);

                let daemonize = Daemonize::new()
                    .pid_file("/tmp/test.pid") // Every method except `new` and `start`
                    .chown_pid_file(true) // is optional, see `Daemonize` documentation
                    .working_directory(base_dir.as_path()) // for default behaviour.          
                    .umask(0o777) // Set umask, `0o027` by default.
                    .stdout(stdout) // Redirect stdout to `/tmp/daemon.out`.
                    .stderr(stderr) // Redirect stderr to `/tmp/daemon.err`.
                    .privileged_action(|| "Executed before drop privileges");

                match daemonize.start() {
                    Ok(_) => {
                        println!("Success, daemonized");
                    }
                    Err(e) => eprintln!("Error, {}", e),
                }
            }
            println!("pid is:{}", std::process::id());
            fs::write("pid", process::id().to_string()).unwrap();
            start("by_daemonize:".to_string());
        }

 

 首先獲取當前的工作目錄,預設情況下 daemonize 會將工作目錄設定為 "/",為了避免許可權問題,我們獲取當前目錄作為守護行程的工作目錄。不知道是什麼原因,在設定了pid_file 後,啟動守護行程時並沒在檔案中有記錄 pid。不過也沒關係,我們可以在外部獲取並記錄守護行程的pid。

兩種方式啟動的守護行程均可在關閉shell的情況下維持程序執行。

從實現上來講,不論是 fork 還是 daemonize 都是 通過unsafe 方式呼叫了 libc api,類 unix 系統大多跑起來沒問題,windows 系統作者沒有驗證。

本期關於守護行程的話題就聊到這兒。

咱們下期見。

作者:賈世聞