golang 守护进程(daemon)实例——后台运行,重启进程
需求
-daemon
功能:为任意 Go 程序创建守护进程,使 Go 业务进程脱离终端运行;-forever
功能:创建监控重启进程,使 Go 业务进程被杀死后能够重启;- 不影响业务进程逻辑;
- 以Linux平台为主,其他平台暂不考虑。
分析
创建守护进程首先要了解go语言如何实现创建进程。在 Unix 中,创建一个进程,通过系统调用 fork
实现(及其一些变种,如 vfork
、clone
)。在 Go 语言中,Linux 下创建进程使用的系统调用是 clone
。
在 C 语言中,通常会用到 2 种创建进程方法:
fork
pid = fork();
//pid > 0 父进程
//pid = 0 子进程
//pid < 0 出错
程序会从 fork
处一分为二,父进程返回值大于0,并继续运行;子进程获得父进程的栈、数据段、堆和执行文本段的拷贝,返回值等于0,并向下继续运行。通过 fork
返回值可轻松判断当前处于父进程还是子进程。
但在 Go 语言中,没有直接提供 fork
系统调用的封装,如果想只调用 fork,需要通过 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
实现。
execve
execve(pathname, argv, envp);
//pathname 可执行文件路径
//argv 参数列表
//envp 环境变量列表
execve
为加载一个新程序到当前进程的内存,这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。
在 Go 语言中,创建进程方法主要有 3 种:
exec.Command
package main
import (
"os"
"os/exec"
"path/filepath"
"time"
)
func main() {
//判 断当其是否是子进程,当父进程return之后,子进程会被 系统1 号进程接管
if os.Getppid() != 1 {
// 将命令行参数中执行文件路径转换成可用路径
filePath, _ := filepath.Abs(os.Args[0])
cmd := exec.Command(filePath, os.Args[1:]...)
// 将其他命令传入生成出的进程
cmd.Stdin = os.Stdin // 给新进程设置文件描述符,可以重定向到文件中
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Start() // 开始执行新进程,不等待新进程退出
os.Exit(0)
// return
} else {
// dosomething
time.Sleep(time.Second * 10)
}
}
os.StartProcess
if os.Getppid()!=1{
args:=append([]string{filePath},os.Args[1:]...)
os.StartProcess(filePath,args,&os.ProcAttr{Files:[]*os.File{os.Stdin,os.Stdout,os.Stderr}})
os.Exit(0)
}
syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
pid, _, sysErr := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
if sysErr != 0 {
Utils.LogErr(sysErr)
os.Exit(0)
}
可以参考例子:https://studygolang.com/articles/3597
在分析这个例子的时候,方法1和2针对于如何判断该进程是否是子进程,例子的方法是 os.Getppid()!=1
,也就是默认了父进程退出之后,子进程会被1号进程接管。
但在我 Ubuntu Desktop
本地测试时却发现,接管孤儿进程的并不是1号进程,因此考虑到程序稳定性和兼容性,不能够以 ppid
作为判断父子进程的依据。
方法3直接进行了系统调用,虽然可以通过 pid
进行判断父子进程,但该方法过于底层,例子中也没有推荐使用,所以也没有采纳。
1. 守护进程
考虑利用方法1进行进程创建,由于 exec.Command
包含了参数传递,可以通过传入不同的参数,实现判断启动守护进程还是启动业务进程。
func main(){
daemon := flag.Bool("daemon", false, "run in daemon")
if *daemon {//父进程,守护进程
cmd := exec.Command(os.Args[0])
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
}
os.Exit()
} else {//子进程,业务进程
DoSomething()
}
}
运行时存在 -daemon
参数,则运行 exec.Command
生成子进程,在传参时删掉 -daemon
参数,再次进入 main
时就进入子进程业务逻辑了,这时父进程也退出,子进程就被系统进程接管。
2. 重启进程
通过上述分析,基本已经实现了守护进程创建,重启进程就依葫芦画瓢了。
forever := flag.Bool("forever", false, "run forever")
if *forever{
for {
cmd := exec.Command(args[0])
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
}
cmd.Wait()
}
}
在死循环(for{...}
)中,创建新的业务进程,通过 cmd.Wait()
等待业务进程退出状态,如果业务进程退出,则再次循环创建进程。
By the way,重启进程和守护进程是可以解耦合的,可以单独判断参数 -daemon
和 -forever
,不必再进行参数个数判断。
实现
本次实现主要通过方法1进行进程创建,以main函数作为程序入口点,通过传参数不同,来判断父子进程,这样有2个好处:
- 参数不同实现启动不同进程;
- 守护进程和重启进程对业务进程透明,不影响业务进程逻辑。
直接上代码
go-daemon.go
package main
import (
"os"
"os/exec"
"fmt"
"flag"
"log"
"time"
)
const (
DAEMON = "daemon"
FOREVER = "forever"
)
func DoSomething(){
fp, _ := os.OpenFile("./dosomething.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
log.SetOutput(fp)
for{
log.Printf("DoSomething running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
time.Sleep(time.Second * 5)
}
}
func StripSlice(slice []string, element string) []string {
for i := 0; i < len(slice); {
if slice[i] == element && i != len(slice)-1 {
slice = append(slice[:i], slice[i+1:]...)
} else if slice[i] == element && i == len(slice)-1 {
slice = slice[:i]
} else {
i++
}
}
return slice
}
func SubProcess(args []string) *exec.Cmd {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
}
return cmd
}
func main(){
daemon := flag.Bool(DAEMON, false, "run in daemon")
forever := flag.Bool(FOREVER, false, "run forever")
flag.Parse()
fmt.Printf("[*] PID: %d PPID: %d ARG: %s\n", os.Getpid(), os.Getppid(), os.Args)
if *daemon {
SubProcess(StripSlice(os.Args, "-"+DAEMON))
fmt.Printf("[*] Daemon running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
os.Exit(0)
} else if *forever {
for {
cmd := SubProcess(StripSlice(os.Args, "-"+FOREVER))
fmt.Printf("[*] Forever running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
cmd.Wait()
}
os.Exit(0)
} else {
fmt.Printf("[*] Service running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
}
DoSomething()
}
使用
编译
go build -ldflags "-s -w" go-daemon.go
运行
./go-daemon -daemon -forever
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏