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进程仍然在运行!

原因分析

  1. Kill()只终止直接父进程

    Kill()发送SIGKILLcmd.Process对应的进程(这里是/bin/sh),但不会自动杀死由该进程启动的子进程(如uvx)。

    在Unix系统中,这些子进程会挂载到PID 1(init)下继续运行。

  2. 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启动多层子进程的场景,能够有效避免进程残留问题。

posted @ 2025-04-13 18:56  LiusCraft  阅读(110)  评论(0)    收藏  举报