Go中如何彻底终止子进程及其子孙进程
Go中如何彻底终止子进程及其子孙进程
问题背景
在使用Go的exec.Command启动进程时,我发现了一个棘手的问题:虽然主进程能够正常终止,但其启动的子进程的子进程("孙子进程")却仍然在运行。
mcpRunner := fmt.Sprintf("\"%s %s\"", s.Config.Command, strings.Join(s.Config.Args, " "))
cmd := exec.Command("/bin/sh", "-c",
fmt.Sprintf("%s --stdio %s --port %d",
config.COMMAND_SUPERGATEWAY,
mcpRunner,
s.Port))
为什么使用/bin/sh?
因为COMMAND_SUPERGATEWAY程序内部是通过/bin/sh启动子进程的。如果直接启动COMMAND_SUPERGATEWAY命令,环境变量无法正确挂载到/bin/sh上,会导致找不到子程序命令。
具体执行的命令是:
/bin/sh -c supergateway --stdio "uvx mcp-server-time --local-timezone=America/New_York" --port 10000其中uvx mcp-server-time...是supergateway内部执行的子命令。
问题现象
当调用cmd.Process.Kill()时:
supergateway进程会被终止- 但
uvx进程仍然在运行!
原因分析
-
Kill()只终止直接父进程Kill()发送SIGKILL给cmd.Process对应的进程(这里是/bin/sh),但不会自动杀死由该进程启动的子进程(如uvx)。在Unix系统中,这些子进程会挂载到PID 1(
init)下继续运行。 -
Shell的进程组管理
通过
/bin/sh -c "command"启动时,command可能属于新的进程组(PGID),而Kill()默认只针对单个进程(PID)。
解决方案:使用进程组
要彻底终止整个进程树,我们需要使用进程组(Process Group)的概念:
cmd := exec.Command("/bin/sh", "-c",
"supergateway --stdio 'uvx mcp-server-time --local-timezone=America/New_York' --port 10000")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 设置新的进程组
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
// 终止整个进程组(包括子进程)
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil {
log.Printf("Failed to kill process group: %v", err)
}
_ = cmd.Wait() // 回收资源
关键点
Setpgid: true确保/bin/sh和子进程属于同一进程组syscall.Kill(-pid, sig)中的负PID表示终止整个进程组
总结
在Go中管理多级子进程时,单纯使用Kill()可能无法彻底清理所有相关进程。通过设置进程组并发送信号给整个进程组,可以确保干净地终止所有相关进程。
这种方法特别适用于需要通过Shell启动多层子进程的场景,能够有效避免进程残留问题。
浙公网安备 33010602011771号