Golang中exec.Command().Run()挂起问题如何解决
Golang中exec.Command().Run()挂起问题如何解决 我开发了一个Go语言的HTTP服务器。它使用fineuploader将文件上传到我的fuse文件系统。
该fuse文件系统有一个缓存算法。如果缓存已满,文件写入将等待缓存可用,最长等待20分钟。这导致Go HTTP服务器的io.copy()处于等待状态。
同时,Go HTTP服务器有一个例行工作,用于检查某些Linux服务是否处于活动状态。我使用了以下代码:
cmd := exec.Command("systemctl", "is-active", "--quiet", servicename)
err := cmd.Run()
我发现这个例行工作会在cmd.Run()处挂起。虽然不是100%会挂起,但失败率非常高。
我编写了一个最小可复现示例并放到了GitHub上:GitHub - derentw/GoCmdHangExample
我希望看到的是cmd.Output()能够无延迟地运行。
我也尝试了Go 1.14,同样存在这个问题。
更多关于Golang中exec.Command().Run()挂起问题如何解决的实战教程也可以访问 https://www.itying.com/category-94-b0.html
如果在操作系统的shell中执行
$ systemctl is-active <servicename>
它的行为是怎样的?它也会卡住吗?
更多关于Golang中exec.Command().Run()挂起问题如何解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
lutzhorn:
它也会卡住吗?
不,它运行良好。只有 Go 进程会卡住。
这是一个典型的子进程输出管道阻塞问题。当exec.Command()执行命令时,如果子进程产生输出但父进程没有读取,管道缓冲区会被填满,导致子进程在写入更多输出时被阻塞。
在你的场景中,systemctl is-active --quiet命令虽然使用了--quiet参数,但在某些情况下仍可能产生输出(比如错误信息)。当HTTP服务器的io.copy()因缓存满而长时间阻塞时,系统资源紧张可能导致子进程输出无法被及时处理。
以下是解决方案和示例代码:
方案1:使用cmd.CombinedOutput()或cmd.Output()并处理输出
cmd := exec.Command("systemctl", "is-active", "--quiet", servicename)
output, err := cmd.CombinedOutput()
if err != nil {
// 处理错误,output包含标准输出和标准错误
fmt.Printf("命令执行失败: %v, 输出: %s\n", err, output)
}
方案2:显式重定向输出到/dev/null
cmd := exec.Command("systemctl", "is-active", "--quiet", servicename)
cmd.Stdout = nil // 重定向到/dev/null
cmd.Stderr = nil // 重定向到/dev/null
err := cmd.Run()
if err != nil {
// 处理错误
}
方案3:使用带超时的执行
func runCommandWithTimeout(timeout time.Duration, name string, arg ...string) error {
cmd := exec.Command(name, arg...)
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
return err
}
timer := time.AfterFunc(timeout, func() {
cmd.Process.Kill()
})
err := cmd.Wait()
timer.Stop()
return err
}
// 使用示例
err := runCommandWithTimeout(5*time.Second, "systemctl", "is-active", "--quiet", servicename)
方案4:使用context控制超时(Go 1.19+)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "systemctl", "is-active", "--quiet", servicename)
cmd.Stdout = nil
cmd.Stderr = nil
err := cmd.Run()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("命令执行超时")
}
}
方案5:完全控制管道(最可靠的方案)
func runServiceCheck(servicename string) error {
cmd := exec.Command("systemctl", "is-active", "--quiet", servicename)
// 创建管道
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return err
}
// 启动命令
if err := cmd.Start(); err != nil {
return err
}
// 异步读取输出,防止阻塞
done := make(chan error, 1)
go func() {
// 读取并丢弃输出
io.Copy(io.Discard, stdoutPipe)
io.Copy(io.Discard, stderrPipe)
done <- cmd.Wait()
}()
// 设置超时
select {
case err := <-done:
return err
case <-time.After(10 * time.Second):
cmd.Process.Kill()
return fmt.Errorf("命令执行超时")
}
}
对于你的具体场景,建议采用方案2或方案4,因为它们简单有效。如果你的Go版本支持,方案4(使用context)是最佳选择,因为它提供了更好的控制能力。
在你的GitHub示例中,问题同样是由于未读取子进程输出导致的。修复方法如下:
func runCmd() {
cmd := exec.Command("bash", "-c", "echo start; sleep 2; echo end;")
// 关键修复:重定向或处理输出
cmd.Stdout = nil
cmd.Stderr = nil
err := cmd.Run()
if err != nil {
fmt.Println(err)
}
}
这个问题在Go 1.19中得到了改进,exec.Cmd现在默认会将未使用的管道重定向到os.DevNull,但在早期版本中需要显式处理。

