Go中exec.Command执行含nohup的脚本时出现挂起问题
问题:Go执行包含nohup的脚本时进程挂起
场景复现
使用如下Go代码执行系统命令或脚本:
var shell, shellArg string if runtime.GOOS == "windows" { shell = "powershell.exe" shellArg = "-Command" // 确保命令出错时退出 command = fmt.Sprintf("try { %s } catch { exit 1 }", command) } else { shell = "/bin/sh" shellArg = "-c" // 用set -e确保任何错误都会导致退出 command = fmt.Sprintf("set -e; %s", command) } cmd := exec.Command(shell, shellArg, command)
普通命令/脚本执行正常,但遇到特殊场景时会挂起:
- 存在控制脚本
main.sh,其中调用的services.sh使用nohup ./startService.sh &启动WebLogic服务 - 执行
main.sh后,WebLogic服务已启动,main.sh进程也已结束,但Go的exec.Command始终处于挂起状态 - 当把nohup命令改为
nohup ./startService.sh > /dev/null 2>&1 &时,Go执行恢复正常
完整的Go执行代码如下:
// 捕获stdout和stderr var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr Logger.Printf("Executing command: %s", strings.Join(cmd.Args, " ")) // 启动命令 if err := cmd.Start(); err != nil { result.Stderr = fmt.Sprintf("Failed to start command: %v", err) result.ExitCode = 1 return result } done := make(chan error) go func() { done <- cmd.Wait() }() select { case err := <-done: // 命令完成 fmt.Println("Command Completed") if err != nil { if exitError, ok := err.(*exec.ExitError); ok { result.ExitCode = exitError.ExitCode() } else { result.ExitCode = 1 // 非ExitError时用通用错误码 } result.Stderr = stderr.String() } else { result.ExitCode = 0 // 成功 result.Stdout = stdout.String() result.Stderr = stderr.String() } case <-time.After(864000 * time.Second): // 可调整超时时间 // 命令超时 if err := cmd.Process.Kill(); err != nil { result.Stderr = fmt.Sprintf("Failed to kill process: %v", err) result.ExitCode = 1 return result } result.Stderr = "Command timed out" result.ExitCode = 1 return result } result.Stdout = stdout.String() result.Stderr = stderr.String()
原因分析
nohup默认会将子进程的stdout和stderr重定向到nohup.out,但当Go进程通过exec.Command启动shell时,shell会将自身的stdout/stderr管道传递给所有子进程(包括startService.sh)。即使main.sh进程退出,startService.sh作为后台进程仍会继续向这个管道输出内容,而Go代码中用bytes.Buffer绑定了stdout/stderr,会一直等待读取管道中的数据,导致cmd.Wait()阻塞,表现为Go进程挂起。
当添加> /dev/null 2>&1后,子进程的输出被定向到空设备,不再往Go持有的管道写数据,管道会被正常关闭,cmd.Wait()就能正常返回。
解决方案
方案1:脚本层重定向输出(已验证有效)
在services.sh中修改nohup命令,显式将所有输出定向到空设备:
nohup ./startService.sh > /dev/null 2>&1 &
方案2:Go代码中忽略子进程输出
如果不需要捕获脚本的stdout/stderr,可以将cmd.Stdout和cmd.Stderr设置为os.DevNull,避免持有管道:
cmd.Stdout = os.DevNull cmd.Stderr = os.DevNull
方案3:让子进程脱离当前会话
通过syscall让子进程创建新会话,脱离父进程的管道关联(仅适用于类Unix系统):
cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, }
这样子进程的输出不会继承父进程的stdout/stderr管道,即使不重定向也不会导致Go进程挂起。
方案4:修改shell命令,强制后台进程输出重定向
在Go拼接shell命令时,统一处理后台进程的输出,比如:
command = fmt.Sprintf("set -e; %s 2>&1 >/dev/null &", command)
但该方案仅适用于不需要捕获输出的场景。
内容的提问来源于stack exchange,提问作者Karthi1234
相关产品推荐
相关产品推荐

