当我们写完一个服务端程序,需要上线部署的时候,或多或少都会和操作系统的守护进程打交道,毕竟谁也不希望shell关闭既停服。今天我们就来聊聊这个事儿。
最早大家部署应用的通常操作是 “nohup xxxx &”,别说像weblogic 或者其他java 容器有启动脚本,里面其实也差不多;很喜欢 nginx的 -d 参数,或者像redis 配置文件里可以指定是否以守护进程启动。看起来很优雅。
那么,使用rust 写一个服务端程序能不能优雅的使用一个参数指定应用 daemon 模式启动,同时使用stop 方式优雅的停机呢?我们通过一个例子来说说基本的实现方式。
实例代码依然集成在interactcli-rs工程中。
首先来模拟一个启动的服务进程 /src/server/server.rs
1 for i in 0..1000 { 2 println!("{}", prefix.clone() + &i.to_string()); 3 thread::sleep(Duration::from_secs(1)); 4 } 5}
程序每秒输出一个字符串,持续999秒,这个时间足够验证实验结果了。
后台启动有两个实现,分别是利用fork 或 daemonize,这两个crate 实现原理类似,但在使用上稍有不同。
/src/cmd/cmdserver.rs,构建了两个启动子命令,分别来调用 fork 和 daemonize的守护进程启动实现.
1 clap::Command::new("server") 2 .about("server") 3 .subcommand(server_start_byfork()) 4 .subcommand(server_start_bydaemonize()) 5} 6 7pub fn server_start_byfork() -> Command { 8 clap::Command::new("byfork") 9 .about("start daemon by fork crate") 10 .arg( 11 Arg::new("daemon") 12 .short('d') 13 .long("daemon") 14 .action(ArgAction::SetTrue) 15 .help("start as daemon") 16 .required(false), 17 ) 18} 19pub fn server_start_bydaemonize() -> Command { 20 clap::Command::new("bydaemonize") 21 .about("start daemon by daemonize crate") 22 .arg( 23 Arg::new("daemon") 24 .short('d') 25 .long("daemon") 26 .action(ArgAction::SetTrue) 27 .help("start as daemon") 28 .required(false), 29 ) 30}
server 的子命令 byfork 启动 通过 fork 实现的功能,bydaemonize 则调用通过 daemonize 的功能实现。
命令解析的代码在 /src/cmd/rootcmd.rs 文件中。
先来看看基于 fork 的实现:
1 println!("start by fork"); 2 if startbyfork.get_flag("daemon") { 3 let args: Vec<String> = env::args().collect(); 4 if let Ok(Fork::Child) = daemon(true, false) { 5 // 启动子进程 6 let mut cmd = Command::new(&args[0]) 7 for idx in 1..args.len() { 8 let arg = args.get(idx).expect("get cmd arg error!"); 9 // 去除后台启动参数,避免重复启动 10 if arg.eq("-d") || arg.eq("-daemon") { 11 continue; 12 } 13 cmd.arg(arg); 14 15 let child = cmd.spawn().expect("Child process failed to start."); 16 fs::write("pid", child.id().to_string()).unwrap(); 17 println!("process id is:{}", std::process::id()); 18 println!("child id is:{}", child.id()); 19 } 20 println!("{}", "daemon mod"); 21 process::exit(0); 22 } 23 start("by_fork:".to_string()); 24}
首先,通过 Fork::daemon 函数派生出一个子进程;然后解析一下当前命令,去掉 -d 参数,构建一个启动命令,子命令启动,退出父进程。这基本符合操作系统创建守护进程的过程 -- 两次 fork。
再来看看基于 daemonize 的实现:
1 println!("start by daemonize"); 2 let base_dir = env::current_dir().unwrap(); 3 if startbydaemonize.get_flag("daemon") { 4 let stdout = File::create("/tmp/daemon.out").unwrap(); 5 let stderr = File::create("/tmp/daemon.err").unwrap(); 6 7 println!("{:?}", base_dir); 8 9 let daemonize = Daemonize::new() 10 .pid_file("/tmp/test.pid") // Every method except `new` and `start` 11 .chown_pid_file(true) // is optional, see `Daemonize` documentation 12 .working_directory(base_dir.as_path()) // for default behaviour. 13 .umask(0o777) // Set umask, `0o027` by default. 14 .stdout(stdout) // Redirect stdout to `/tmp/daemon.out`. 15 .stderr(stderr) // Redirect stderr to `/tmp/daemon.err`. 16 .privileged_action(|| "Executed before drop privileges"); 17 18 match daemonize.start() { 19 Ok(_) => { 20 println!("Success, daemonized"); 21 } 22 Err(e) => eprintln!("Error, {}", e), 23 } 24 } 25 println!("pid is:{}", std::process::id()); 26 fs::write("pid", process::id().to_string()).unwrap(); 27 start("by_daemonize:".to_string()); 28 }
首先获取当前的工作目录,默认情况下 daemonize 会将工作目录设置为 "/",为了避免权限问题,我们获取当前目录作为守护进程的工作目录。不知道是什么原因,在配置了pid_file 后,启动守护进程时并没在文件中有记录 pid。不过也没关系,我们可以在外部获取并记录守护进程的pid。
两种方式启动的守护进程均可在关闭shell的情况下维持进程运行。
从实现上来讲,不论是 fork 还是 daemonize 都是 通过unsafe 方式调用了 libc api,类 unix 系统大多跑起来没问题,windows 系统作者没有验证。
本期关于守护进程的话题就聊到这儿。
咱们下期见。
作者:贾世闻
