• 文盘Rust -- 把程序作为守护进程启动


    当我们写完一个服务端程序,需要上线部署的时候,或多或少都会和操作系统的守护进程打交道,毕竟谁也不希望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 = 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 系统作者没有验证。

    本期关于守护进程的话题就聊到这儿。

    咱们下期见。

    作者:贾世闻

     

  • 相关阅读:
    Qt应用软件【文件篇】json文件读写
    货币转换
    (个人笔记)EDEM耦合Recurdyn流程
    对于自动化测试的认知,大多数人容易理解错误的几个误区
    08c++呵呵老师【给子弹添加爆炸效果】
    使用Supervisor进行监控进程并实现自动重启
    内容创作者抖音直播涨粉变现7个小技巧
    30岁以上的程序员该何去何从?
    BEV感知PETR-V1和PETR-V2
    学习 Rust 的第十一天:如何使用模块
  • 原文地址:https://www.cnblogs.com/Jcloud/p/16850261.html