golang exec.Command 导致大量defunct(僵尸)进程
cmd := exec.Command(*binPath, opt.binCmd()...)
//cmd.Stderr = os.Stderr
//cmd.Stdout = os.Stdout
if err := cmd.Start(); err != nil {
fmt.Printf("[err] exec.Command err:%s, cmd:%s \n", err, cmd.String())
return
}
这么一段程序引发的大量defunct(僵尸)进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
根据定义, 肯定是父进程没有调用wait操作导致的, 然后改用cmd.Run阻塞式调用解决, 也可以用cmd.Start() 加 cmd.Wait()解决, 思路一样
cmd.Start 到底做了什么:
打开源码,
func (c *Cmd) Start() error {
//....检查文件
//创建标准输入, 标准输出, 错误输出文件描述符
c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles))
type F func(*Cmd) (*os.File, error)
for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} {
fd, err := setupFd(c)
if err != nil {
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return err
}
c.childFiles = append(c.childFiles, fd)
}
c.childFiles = append(c.childFiles, c.ExtraFiles...)
envv, err := c.envv()
if err != nil {
return err
}
// 启动子进程, 返回进程pid
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
Dir: c.Dir,
Files: c.childFiles,
Env: addCriticalEnv(dedupEnv(envv)),
Sys: c.SysProcAttr,
})
//...一系列关闭动作
return nil
}
继续看os.StartProcess函数, 核心代码在startProcess函数中, startProcess 主是要组装数据, 继续到syscall.StartProcess中,调用forkExec
// StartProcess wraps ForkExec for package os.
func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) {
pid, err = forkExec(argv0, argv, attr)
return pid, 0, err
}
func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
var p [2]int
var n int
var err1 Errno
var wstatus WaitStatus
//...转换和检查
ForkLock.Lock()
// Allocate child status pipe close on exec.
if err = forkExecPipe(p[:]); err != nil {
goto error
}
// 启动并执行子程序
pid, err1 = forkAndExecInChild(argv0p, argvp, envvp, chroot, dir, attr, sys, p[1])
if err1 != 0 {
err = Errno(err1)
goto error
}
ForkLock.Unlock()
// 从管道p[0]中读取错误信息
Close(p[1])
n, err = readlen(p[0], (*byte)(unsafe.Pointer(&err1)), int(unsafe.Sizeof(err1)))
Close(p[0])
}
主要看这几个函数, 其中 forkExecPipe, 要了解这个函数, 了解这两个概念即可
一个linux的pipe, 创建pipe需要两个文件描述符, 0对应标准输入,1对应标准输出一样, 一个负责写, 一个负责读,
一个是FD_CLOEXEC, fork子进程后执行exec时就关闭文件句柄, 即所谓的 close-on-exec
// Try to open a pipe with O_CLOEXEC set on both file descriptors.
func forkExecPipe(p []int) error {
err := Pipe(p)
if err != nil {
return err
}
_, err = fcntl(p[0], F_SETFD, FD_CLOEXEC)
if err != nil {
return err
}
_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
return err
}
管道创建好之后, 核心执行的代码在forkAndExecInChild 中 这个代码中主要是一些系统调用, 基本就是fork一个子进程, 然后调用指定程序, 并将错误写入管道