Golang守护进程用法示例分析
作者:dkjhl
前言
golang实现守护进程,包含功能:
1. 守护进程只创建一次
2. 平滑创建业务进程
3. 业务进程挂起,守护进程能监听,并重启新启业务进程
4. 守护进程退出,也能保证业务进程退出
5. 业务进程≈子进程
6. 不影响业务进程逻辑
7. 以Linux平台为主,其他平台暂时没有实施条件
分析
上一篇博文讨论过如何以脚本的形式创建守护进程,这篇讨论如何以纯golang脚本实现守护进程的功能
- 在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。
- 在 Go 语言中,Linux 下创建进程使用的系统调用是 clone 。
在 C 语言中,通常会用到 2 种创建进程方法:
fork
pid = fork(); //pid > 0 父进程 //pid = 0 子进程 //pid < 0 出错
程序会从 fork 处一分为二,父进程返回值大于0,并继续运行;子进程获得父进程的栈、数据段、堆和执行文本段的拷贝,返回值等于0,并向下继续运行。通过 fork 返回值可轻松判断当前处于父进程还是子进程。
execve
execve(pathname, argv, envp); //pathname 可执行文件路径 //argv 参数列表 //envp 环境变量列表
execve 为加载一个新程序到当前进程的内存,这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。
在 Go 语言中,创建进程方法主要有 3 种:
exec.Command
//判 断当其是否是子进程,当父进程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) }
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) }
方法1和方法2通过 os.Getppid()!=1进行判断是否子进程,默认父进程退出之后,子进程会被1号进程接管。
但据Ubuntu Desktop 本地测试,接管孤儿进程的并不是1号进程,因此考虑到程序稳定性和兼容性,不能够以 ppid 作为判断父子进程的依据。
方法3直接进行了系统调用,虽然可以通过 pid 进行判断父子进程,但该方法过于底层。
综上,以exec.Command方式,通过控制参数实现守护进程
实现
func main() { // ------------------------ 守护进程 start ------------------------ basePath, _ := os.Getwd() baseDir := filepath.Dir(basePath) fmt.Println(fmt.Sprintf("basePath is %s and baseDir is %s", basePath, baseDir)) // step1 // 创建监听退出chan c := make(chan os.Signal) // 监听指定信号 ctrl+c kill signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) go func() { for s := range c { switch s { case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: utils.StopBusinessProcess(fmt.Sprintf("go_start | grep business")) os.Exit(0) default: fmt.Println("test stop others...") } } }() fmt.Println(fmt.Sprintf("os.args is %v", os.Args)) join := strings.Join(os.Args, "") // step2 if !strings.Contains(join, "-daemon") { fmt.Println("enter daemon branch...") isE, ierr := utils.CheckProRunning("go_start | grep daemon") if ierr != nil { fmt.Println("check daemon process failed, " + ierr.Error()) return } if isE { fmt.Println("daemon process exist!") } else { fmt.Println("start daemon process...") // 启动守护进程 cmd := exec.Command(os.Args[0], "-c", os.Args[2], "-d", os.Args[4], "-e", os.Args[6], "-daemon") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr strerr := cmd.Start() if strerr != nil { fmt.Println("start daemon process fail," + strerr.Error()) return } fmt.Println("start daemon process success!") time.Sleep(time.Second * 2) daePid := cmd.Process.Pid isDae, daeErr := utils.CheckProRunning("go_start | grep daemon") if daeErr != nil { fmt.Println("check daemon process failed, " + daeErr.Error()) return } if isDae { fmt.Println(fmt.Sprintf("start daemon process success, pid is %d", daePid)) return } else { fmt.Println("warning! start business process fail...") } } } // step3 join = strings.Join(os.Args, "") if strings.Contains(join, "-daemon") { fmt.Println("enter business branch...") for { exist, checkerr := utils.CheckProRunning("go_start | grep business") if checkerr != nil { fmt.Println("check business failed, " + checkerr.Error()) return } if exist { fmt.Println("business process exist!") time.Sleep(time.Second * 5) continue } fmt.Println("start business process...") command := exec.Command(fmt.Sprintf(fmt.Sprintf("%s/go_start", basePath), "-business", "-c", os.Args[2], "-d", os.Args[4], "-e", os.Args[6])) command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if comerr := command.Start(); comerr != nil { fmt.Println("start business process failed, " + comerr.Error()) return } time.Sleep(time.Second * 5) businessPid := command.Process.Pid exist, checkerr = utils.CheckProRunning("go_start | grep business") if checkerr != nil { fmt.Println("check business process failed, " + checkerr.Error()) return } if exist { fmt.Println(fmt.Sprintf("start business process suceess, pid is %d", businessPid)) } else { fmt.Println("warning! start business process fail...") } } } // ------------------------ 守护进程 end ------------------------ // ------------------------ 业务进程 start ------------------------ fmt.Println("hello, welcome to business detail!") }
相关工具方法:
package utils import ( "os" "fmt" "go_start/core/global" "os/exec" "runtime" "strconv" "strings" "syscall" ) func StopBusinessProcess(serverName string) { global.G_LOG.Info("start to stop business...") pid, _ := GetPid(serverName) if pid > 0 { global.G_LOG.Info(fmt.Sprintf("stop %s ...", serverName)) syscall.Kill(pid, syscall.SIGKILL) global.G_LOG.Info(fmt.Sprintf("stop business success, pid is %d", pid)) } } //根据进程名判断进程是否运行 func CheckProRunning(serverName string) (bool, error) { a := `ps -ef|grep ` + serverName + `|grep -v grep|awk '{print $2}'` pid, err := runCommand(a) if err != nil { return false, err } return pid != "", nil } //根据进程名称获取进程ID func GetPid(serverName string) (pid int, err error) { a := `ps -ef|grep ` + serverName + `|grep -v grep|awk '{print $2}'` var pidStr string if pidStr, err = runCommand(a); err != nil { return } pid, err = strconv.Atoi(pidStr) return } func runCommand(cmd string) (string, error) { if runtime.GOOS == "windows" { return runInWindows(cmd) } else { return runInLinux(cmd) } } func runInWindows(cmd string) (string, error) { result, err := exec.Command("cmd", "/c", cmd).Output() if err != nil { return "", err } return strings.TrimSpace(string(result)), err } func runInLinux(cmd string) (string, error) { result, err := exec.Command("/bin/sh", "-c", cmd).Output() if err != nil { return "", err } return strings.TrimSpace(string(result)), err }
说明
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)实现
附录
以下是关于信号量的一个记录,当作参考文档
信号 | 值 | 动作 | 说明 |
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发 |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略) SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发 SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发 |
到此这篇关于Golang守护进程用法示例分析的文章就介绍到这了,更多相关Golang守护进程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!