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()cmd.Processに対応するプロセス(ここでは/bin/sh)にSIGKILLを送信しますが、そのプロセスが起動した子プロセス(例えばuvx)は自動的に終了しません。

    Unixシステムでは、これらの子プロセスはPID 1(init)にアタッチして実行を続けます。

  2. シェルのプロセスグループ管理

    /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("プロセスグループの終了に失敗: %v", err)
}
_ = cmd.Wait() // リソースを回収

重要ポイント

  • Setpgid: trueにより/bin/shと子プロセスが同じプロセスグループに属するようになります
  • syscall.Kill(-pid, sig)の負のPIDは、プロセスグループ全体を終了させることを意味します

タグ: Go言語 プロセス管理 シグナル処理 exec.Command syscall

8月16日 17:22 投稿