用 os/exec 调外部命令,最容易出问题的不是启动,而是收尾:不 Wait 会留下僵尸进程,管道没读完就 Wait 会丢数据或卡死,子进程的子进程会让 Output 迟迟不返回,Context 超时也杀不干净。这篇把这些情形一个个跑出来,并验证 WaitDelay 能不能兜底。
Start 之后不调用 Wait,子进程已经退出 200 ms:ps 显示 54033 Z <defunct> 调用 cmd.Wait() 之后:ps 显示 (进程表里已经没有这个 pid)
子进程退出后,内核保留它的退出状态,等父进程来取。不调用 Wait,ps 里就是状态 Z(<defunct>);调用后进程表项消失。长期运行的服务里反复 Start 而不 Wait,僵尸会越积越多。
先 Wait 再读 StdoutPipe:读到 "",err = read |0: file already closed 先读完 StdoutPipe 再 Wait:读到 "hello\nworld\n",读 err = <nil>,Wait err = <nil>
文档写明:Wait 会在看到命令退出后关闭管道,所以必须先读完再 Wait。先 Wait 的实验读到空和 “file already closed”;使用 StdoutPipe 时也不能调用 Run。
子进程写 200000 字节,父进程没人读管道:1.5 秒后 Wait 仍然没有返回(子进程卡在 write 上:54052 S head)
管道有缓冲上限,子进程写 200000 字节而父进程不读,子进程阻塞,Wait 也就一直不返回。要么及时读,要么把输出交给 cmd.Stdout(由 os/exec 起 goroutine 去读)。
sh 立刻退出,但留下一个后台 sleep 1 抱着 stdout:cmd.Output() 用了 1009 ms 才返回,输出 "hi\n",err = <nil> 同样的命令设 cmd.WaitDelay = 200 ms:206 ms 返回,输出 "hi\n",err = exec: WaitDelay expired before I/O complete(是 ErrWaitDelay:true)
sh 已经退出,但它留下的后台 sleep 1 还持有 stdout 的写端,Output 要等到 EOF 才返回(1009 ms)。给 cmd.WaitDelay 设 200 ms 后 206 ms 返回,同时返回 ErrWaitDelay(输出仍是 hi)。
CommandContext(超时 200 ms),sh -c 'sleep 2; echo done':2017 ms 后返回,输出 "",err = signal: killed CommandContext(超时 200 ms),sh -c 'sleep 2 & wait':2015 ms 后返回,输出 "",err = signal: killed(sh 被杀了,孙进程 sleep 还抱着 stdout)
200 ms 超时到了,CommandContext 只给直接的子进程(这里是 sh)发 Kill,sh 的孙进程 sleep 2 仍然活着、抱着管道,所以 Output 一直等到约 2 秒(2017 ms)才返回,错误是 signal: killed。配合 WaitDelay,或者让子进程自成进程组再整体杀掉,才能真正按超时返回(进程组的做法本文没有实验)。
exec.Command("/usr/bin/true").Run() 50 次:最小 1.5 ms / 中位 2.1 ms / 最大 5.8 ms启动并等待一个什么都不做的进程,中位数 2.1 ms。批量调用外部命令时,这个固定成本要算进去。
StdoutPipe returns a pipe that will be connected to the command's standard output when the command starts. Cmd.Wait will close the pipe after seeing the command exit, so most callers need not close the pipe themselves. It is thus incorrect to call Wait before all reads from the pipe have completed. For the same reason, it is incorrect to call Cmd.Run when using StdoutPipe. See the example for idiomatic usage. If WaitDelay is non-zero, it bounds the time spent waiting on two sources of unexpected delay in Wait: a child process that fails to exit after the associated Context is canceled, and a child process that exits but leaves its I/O pipes unclosed. If WaitDelay is zero (the default), I/O pipes will be read until EOF, which might not occur until orphaned subprocesses of the command have also closed their descriptors for the pipes.
文档对 WaitDelay 的描述和实验吻合:默认零值会一直读到所有持有管道的进程关闭它们的描述符。