在golang中,我通常可以将context.WithTimeout()
与exec.CommandContext()
结合使用,以获得一个在超时后自动终止(使用SIGKILL)的命令。
但是我遇到了一个奇怪的问题,如果我用sh -c
* 和 * Package 命令,通过设置cmd.Stdout = &bytes.Buffer{}
缓冲命令的输出,超时不再起作用,命令永远运行。
为什么会发生这种情况?
下面是一个最小可重复的示例:
package main
import (
"bytes"
"context"
"os/exec"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
cmdArgs := []string{"sh", "-c", "sleep infinity"}
bufferOutputs := true
// Uncommenting *either* of the next two lines will make the issue go away:
// cmdArgs = []string{"sleep", "infinity"}
// bufferOutputs = false
cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...)
if bufferOutputs {
cmd.Stdout = &bytes.Buffer{}
}
_ = cmd.Run()
}
我用Linux标记了这个问题,因为我只验证了这在Ubuntu 20.04上发生,我不确定它是否会在其他平台上重现。
1条答案
按热度按时间6pp0gazn1#
我的问题是,当上下文超时时,子
sleep
进程没有被杀死。父进程sh
被杀死,但子进程sleep
被保留。这通常仍然允许
cmd.Wait()
调用成功,但问题是cmd.Wait()
等待进程退出 * 和 * 复制输出。因为我们已经分配了cmd.Stdout
,所以我们必须等待sleep
进程的stdout管道的读端关闭,但它永远不会关闭,因为进程仍在运行。为了杀死子进程,我们可以通过设置
Setpgid
位来启动进程作为自己的进程组领导者,这将允许我们使用其 negative PID杀死进程以及任何子进程。下面是我提出的
exec.CommandContext
的替代品,它可以做到这一点:自从编写这段代码以来,我遇到过这样的情况,子进程有时想加入自己的进程组,而setpgid技巧不再起作用,因为它不会杀死那些新进程组中的进程。更健壮的解决方案可能是使用类似
go-ps
的东西手动遍历进程树,并为每个后代进程使用以下伪代码: