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