前言&背景
平时在做一些开发时难免要调一些shell脚本或者外部程序,golang提供了exec包很方便的帮我们解决了这个问题。但是当外部程序或者shell脚本夯死就使得我们自身的程序很不稳定。与此同时,当我们已经感知到程序脚本运行出现问题时,我们可能需要立刻对程序进行杀死的操作,但是当我们很自然的想到cmd.Process.Kill()时,我们又遇上了另外一个问题,因为这个操作并没有将子进程kill掉,所以我们的需求就出来了,那就是:
Kill进程及其子进程
首先给出解决方案,那就是kill掉进程组
给出一个有测试和原理分析的相关链接,我这里也来说下具体用法。
cmd := exec.Command("/bin/sh", "-c", "...........")
// Go会将PGID设置成与PID相同的值
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
这里来解释一下上面三行
- 新建一个cmd
- 设置该cmd成为一个新的进程组
- 调用系统Kill方法杀死整个进程组
至此就完成了进程及其所有子进程的删除。
延伸
上面是一个直接杀死进程的代码,而我们通常的场景是脚本执行超时了,或者外部程序控制要杀死一个进程。针对这样的情况,然后我们再结合上面的代码分析,要杀死进程时传入的是-cmd.Process.Pid
, -
代表是传入的是进程组号,cmd.Process.Pid
是具体的值,这里要注意的是cmd.Process是否为nil
的问题了。
提供一个使用场景,很多API调用都会执行一些shell脚本,而且每个shell命令的执行都要可控(即就是能保存PID,并且能杀死)。那么如何保存,其实我们就可以用一个map,把cmd指针和本次调用任务保存起来,这里的map需要保持同步,自己封装一下sync.RWMutex
就可以了。假设我们的任务每次都是有ID的,这里我们就申请一个这样的map:Data map[int64](*exec.Cmd)
,再次提醒,要保证同步就要把这里的Data
和sync.RWMutex
封装在一起。
那么问题来了:
如何保证我们杀死的一定是一个存在的并且正在运行的程序?
这里也提供了解决方法:
for {
if cmd.Process != nil {
//加锁map、保存PID、解锁map
break
}
time.Sleep(1 * time.Nanosecond)
}
你没有看错,就是轮询,只要cmd.Process
不为nil
,那就证明程序开始运行了。那我们又如何判断程序依旧在运行,没有运行结束呢(我们总不能把一个死了的进程再次杀一会吧,是不是有点不道德,说不定还会引起进程的误杀)?
如下是判断进程是否结束的代码:
for {
if cmd.ProcessState != nil {
//map加锁、删除map中的key、解锁map
break
}
time.Sleep(1 * time.Nanosecond)
}
判断的依据是cmd.ProcessState
是否为nil
,若不为nil
就代表程序已经运行结束了,并且将结束的相关信息已经保存进cmd.ProcessState
。
就是这样,以较为优雅的方式控制exec程序的生死 :-)