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一個子進程, 然后調用指定程序, 並將錯誤寫入管道


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM