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一个子进程, 然后调用指定程序, 并将错误写入管道

posted @ 2021-03-04 20:51  screte  阅读(1837)  评论(0编辑  收藏  举报