zoukankan      html  css  js  c++  java
  • 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 
    ", 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一个子进程, 然后调用指定程序, 并将错误写入管道

  • 相关阅读:
    2020-2021-1 20201314 《信息安全专业导论》第三周学习总结
    罗马数字转阿拉伯数字
    BASE64编码-20201314黄斯阳
    学期(2020-2021-1) 学号(20201314) 《信息安全专业导论》第2周学习总结
    师生关系
    快速浏览教材 。
    浏览教材的疑问
    2020-2021-1 20201314 《信息安全专业导论》第一周学习总结
    第四周作业补交
    第四周作业
  • 原文地址:https://www.cnblogs.com/leescre/p/14482909.html
Copyright © 2011-2022 走看看