前言
golang实现守护进程,包含功能:
1. 守护进程只创建一次
2. 平滑创建业务进程
3. 业务进程挂起,守护进程能监听,并重启新启业务进程
4. 守护进程退出,也能保证业务进程退出
5. 业务进程≈子进程
6. 不影响业务进程逻辑
7. 以Linux平台为主,其他平台暂时没有实施条件
分析
上一篇博文讨论过如何以脚本的形式创建守护进程,这篇讨论如何以纯golang脚本实现守护进程的功能
- 在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。
- 在 Go 语言中,Linux 下创建进程使用的系统调用是 clone 。
在 C 语言中,通常会用到 2 种创建进程方法:
fork
程序会从 fork 处一分为二,父进程返回值大于0,并继续运行;子进程获得父进程的栈、数据段、堆和执行文本段的拷贝,返回值等于0,并向下继续运行。通过 fork 返回值可轻松判断当前处于父进程还是子进程。
execve
execve 为加载一个新程序到当前进程的内存,这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。
在 Go 语言中,创建进程方法主要有 3 种:
exec.Command
os.StartProcess
syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
方法1和方法2通过 os.Getppid()!=1进行判断是否子进程,默认父进程退出之后,子进程会被1号进程接管。
但据Ubuntu Desktop 本地测试,接管孤儿进程的并不是1号进程,因此考虑到程序稳定性和兼容性,不能够以 ppid 作为判断父子进程的依据。
方法3直接进行了系统调用,虽然可以通过 pid 进行判断父子进程,但该方法过于底层。
综上,以exec.Command方式,通过控制参数实现守护进程
实现
相关工具方法:
说明
1、启动go_start二进制文件,方式:./go_start -c param1 -d param2 -e param3,这里第一次进入main方法
2、main方法中,os.Args = [./go_start -c param1 -d param2 -c param3],此时不包含"-daemon"参数,进入step2,走创建守护进程代码分支,执行创建守护进程,exec.Command(./go_start -c param1 -d param2 -e param3 -daemon),第二次进入main方法
3、main方法中,os.Args = [./go_start -c param1 -d param2 -c param3 -daemon],此时包含"-daemon",进入step3,走创建业务进程分支,执行创建业务进程,exec.Command(./go_start -c param1 -d param2 -e param3);此时守护进程存在,每隔5秒监听一次业务进程是否存在,如果存在则不操作;不存在则重新执行创建业务进程exec.Command(./go_start -c param1 -d param2 -e param3);
4、执行具体的业务进程逻辑
验证
ps -ef | grep go_start
]$ 110 1 ./go_start -c param1 -d param2 -c param3 -- ①
]$ 111 1 ./go_start -c param1 -d param2 -c param3 -daemon -- ②
]$ 112 111 ./go_start -business -c param1 -d param2 -c param3 -- ③
刚开始会出现三个进程,假设进程id如上,一会之后①会消失,这是正常的,因为刚开始的启动就是①,然后只剩下进程②和③
]$ 111 1 ./go_start -c param1 -d param2 -c param3 -daemon -- ②
]$ 112 111 ./go_start -business -c param1 -d param2 -c param3 -- ③
验证kill业务进程:会启动新的业务进程,守护进程不变;所以执行:kill 112
]$ 111 1 ./go_start -c param1 -d param2 -c param3 -daemon -- ②
]$ 112 111 [go_start] <defunct> -- ③'
]$ 113 111 ./go_start -business -c param1 -d param2 -c param3 -- ③
这里kill 112后,会出现一个僵尸进程,不影响实际业务进程的创建和运行,不需要理会;假设新创建的业务进程pid为113
验证kill守护进程:整个程序退出,也就是执行ps -ef | grep go_start后,没有对应的守护进程和业务进程,同时僵尸进程也会消失;得益于以下代码,进程组
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
综上
纯golang语言形式实现了守护进程,针对启动业务进程,优化点:可以使用go func(){}()协程方式启动更优雅,这里先不实施,待后续有空改进;
缺点:依然要通过参数控制守护进程和业务进程,-daemon -business,期望统一起来,不用参数控制
放在(3)实现
附录
以下是关于信号量的一个记录,当作参考文档