Go语言中Kill子进程的正确姿势

场景

我们在编写部署系统的时候,通常需要在机器上部署一个agent,用来执行部署脚本,为了防止部署脚本写的有问题,长时间hang住,我们通常会为脚本的执行设置一个超时时间,到了时间之后就kill掉该脚本的进程。如果是Go语言实现,脑袋里应该立马浮现出os/exec包、cmd.Process.Kill()这样的手段。但是,如果部署脚本中又调用了其他脚本,即子进程又fork出更多子进程的时候,这招就不好使了。

简单来说,就是cmd.Process.Kill()无法杀死子进程。

知识储备

pstree -g		#查看进程树和每个进程的PGID

问题验证

下面我们写段代码来简单验证一下

一般情况下:

package main

import (
	"fmt"
	"os/exec"
	"time"
)

func main() {
	cmd := exec.Command("sleep", "5")		//睡眠5s
	start := time.Now()			//记录启动时间
	time.AfterFunc(3*time.Second, func() { cmd.Process.Kill() })		//3s后将此进程杀死
	err := cmd.Run()			//运行该命令
	fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err)		//输出进程ID、运行时间、错误
}

打开终端查看:发现进程能被kill掉

root@flight:~$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root  1804   0.0  0.0  4278124    996 s000  R+    4:10下午   0:00.00 ps au
didi  1158   0.0  0.0  4296892   1284 s000  S     4:08下午   0:00.03 -bash
didi  1798   0.0  0.0  4270348    564 s001  S+    4:10下午   0:00.00 sleep 5
root@flight:~$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root  1819   0.0  0.0  4268908    972 s000  R+    4:10下午   0:00.00 ps au
didi  1158   0.0  0.0  4296892   1300 s000  S     4:08下午   0:00.03 -bash
root@flight:~$

当进程fork出子进程:

package main

import (
	"fmt"
	"os/exec"
	"time"
)

func main() {
	cmd := exec.Command("/bin/sh", "-c", "watch date > date.txt")	//watch进程fork出了其他子进程
	start := time.Now()			//记录启动时间
	time.AfterFunc(3*time.Second, func() { cmd.Process.Kill() })		//3s后将此进程杀死
	err := cmd.Run()			//运行该命令
	fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err)		//输出进程ID、运行时间、错误
}

输出结果:发现同样运行了3s被kill

[root@localhost ~]# go run test.go 
pid=16860 duration=3.001284491s err=signal: killed		#同样运行了3s被kill
[root@localhost ~]#

但是查看用户进程会发现不一样:

[root@localhost ~]# ps -af
UID         PID   PPID  C STIME TTY          TIME CMD
root       2409   2277  0 17:07 pts/1    00:00:01 top
root       5118   4953  0 17:09 pts/3    00:00:00 top
root      16804   2269 14 17:14 pts/0    00:00:00 go run test.go
root      16855  16804  0 17:14 pts/0    00:00:00 /tmp/go-build276131739/b001/exe/test
root      16860  16855  0 17:14 pts/0    00:00:00 /bin/sh -c watch date > date.txt
root      16861  16860  0 17:14 pts/0    00:00:00 watch date
root      16954   4919  0 17:14 pts/2    00:00:00 ps -af
[root@localhost ~]# 
[root@localhost ~]# 
[root@localhost ~]# ps -af
UID         PID   PPID  C STIME TTY          TIME CMD
root       2409   2277  0 17:07 pts/1    00:00:01 top
root       5118   4953  0 17:09 pts/3    00:00:00 top
root      16861      1  0 17:14 pts/0    00:00:00 watch date
root      17169   4919  0 17:14 pts/2    00:00:00 ps -af

现象:

  • 程序运行3s后/bin/sh -c watch date > date.txt被kill,但是watch date依然存在。
  • watch date的父进程是1号进程。

问题原因

Go是使用kill(2)向sh进程的PID发了一个KILL信号,但没有发给watch进程,sh进程被kill之后,导致watch进程变成孤儿进程。

解决方案

kill(2)不但支持向单个PID发送信号,还可以向进程组发信号,我们只要把sh进程及其所有子进程放到一个进程组里,就可以批量Kill了。关键是PGID的设置,默认情况下,子进程会把自己的PGID设置成与父进程相同,所以,我们只要设置了sh进程的PGID,所有子进程也就相应的有了PGID。

注意:传递进程组PGID的时候要使用负数的形式。

注意:下面这种方式适合非su - <user> command执行命令的方式,否则杀死父进程后,子进程将被托管成为孤儿进程。因为他们的PGID不一样,即使设置了cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}也没有用。正确的方式是采用exec包自带的方式来指定执行用户

package main

import (
    "fmt"
    "os/exec"
    "syscall"
    "time"
)

func main() {
    cmd := exec.Command("/bin/sh", "-c", "watch date > date.txt")
    
    // Go会将PGID设置成与PID相同的值
    cmd.SysProcAttr = &syscall.SysProcAttr{}
	cmd.SysProcAttr.Setpgid=true
	
    start := time.Now()
    time.AfterFunc(3*time.Second, func() { syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) })   //想要杀死整个进程组,而不是单个进程,需要传递负数形式。
    err := cmd.Run()
    fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err)
}

如果想要指定用户:

sysuser, err := user.Lookup("user1") // 通过用户名来获取用户信息
if err != nil {
	fmt.Println(err)
}
uid, err := strconv.Atoi(sysuser.Uid) // 将UID的类型转换成 uint32
if err != nil {
	fmt.Println(err)
}
gid, err := strconv.Atoi(sysuser.Gid) // 将GID的类型转换成 uint32
if err != nil {
	fmt.Println(err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{
	Uid:         uint32(uid),
	Gid:         uint32(gid),
}

参考文档

链接:Go语言中Kill子进程的正确姿势

posted @ 2021-02-15 17:32  NetRookieX  阅读(40)  评论(0编辑  收藏  举报