前言
目前在构思阶段
现状
Go目前的后台守护进程设计都相对比较简单
目标
- 实现后台运行
- 实现daemon(守护进程)
- 实现daemon模式下的停止(stop)、重启(restart)操作
实现思路
使用一个启动器程序(Launcher)启动需要守护的程序(A)
但是我们不希望出现多出一个启动器(Launcher),能不能只有程序A呢?
关键点
- 父进程和子进程的感知
- 守护进程(daemon)
解决思路
父进程和子进程的感知
我们希望编程编译后的程序只有一个,并非启动器。所以实现方案就是采用自己启动自己的方案,并且守护自己的子进程。难点在于子程序要知道自己是子程序,父程序知道自己是父程序。
开源库 github.com/sevlyar/go-daemon 巧妙的使用了环境变量,用来区分子进程和父进程。这种方案对go程序影响更小,产生冲突的可能性更小,也避免了使用者对参数变化的迷惑。其原理是利用的是exec.Cmd的Env属性设置子进程的环境变量时,添加一个特殊的环境变量,用以标记子程序。用这个思路,我们把上面的例子修正一下。模仿C语言里的fork,返回一个可用用于判断是子进程还是父进程的数据。
//示例:self1.go
package main
import (
"fmt"
"log"
"os"
"os/exec"
"time"
)
func main() {
cmd, err := background("/tmp/daemon.log")
if err != nil {
log.Fatal("启动子进程失败:", err)
}
//根据返回值区分父进程子进程
if cmd != nil { //父进程
log.Println("我是父进程:", os.Getpid(), "; 启动了子进程:", cmd.Process.Pid, "; 运行参数", os.Args)
return //父进程退出
} else { //子进程
log.Println("我是子进程:", os.Getpid(), "; 运行参数:",os.Args)
}
//以下代码只有子进程会执行
log.Println("只有子进程会运行:", os.Getpid(), "; 开始...")
time.Sleep(time.Second * 20) //休眠20秒
log.Println("只有子进程会运行:", os.Getpid(), "; 结束")
}
//@link https://www.zhihu.com/people/zh-five
func background(logFile string) (*exec.Cmd, error) {
envName := "XW_DAEMON" //环境变量名称
envValue := "SUB_PROC" //环境变量值
val := os.Getenv(envName) //读取环境变量的值,若未设置则为空字符串
if val == envValue { //监测到特殊标识, 判断为子进程,不再执行后续代码
return nil, nil
}
/*以下是父进程执行的代码*/
//因为要设置更多的属性, 这里不使用`exec.Command`方法, 直接初始化`exec.Cmd`结构体
cmd := &exec.Cmd{
Path: os.Args[0],
Args: os.Args, //注意,此处是包含程序名的
Env: os.Environ(), //父进程中的所有环境变量
}
//为子进程设置特殊的环境变量标识
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envName, envValue))
//若有日志文件, 则把子进程的输出导入到日志文件
if logFile != "" {
stdout, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
log.Println(os.Getpid(), ": 打开日志文件错误:", err)
return nil, err
}
cmd.Stderr = stdout
cmd.Stdout = stdout
}
//异步启动子进程
err := cmd.Start()
if err != nil {
return nil, err
}
return cmd, nil
}
实现daemon模式下的停止(stop)、重启(restart)操作
如果需要在daemon模式下的停止、重启的操作就需要一个办法给守护进程发送对应的操作动作,由于Go是可以协程运行的,所以可以在守护进程
里面启动RPC监听协程
后在启动业务进程
,即可在RPC监听协程
里面对业务进程
进行操作。
不过会出现RPC监听占用端口,所以可以使用Unix socket
实现通讯,在程序所在目录创建 Unix socket描述文件