golang 设置进程退出时kill所有子进程

在 Go 语言中使用 os/exec.Command 启动子进程时,默认情况下子进程不会随父进程(当前进程)的退出而自动终止,因为子进程会被系统(如 init 或系统进程管理器)收养并继续运行。要实现"父进程退出时自动终止子进程"的功能,需要根据操作系统采用不同的策略。没有完美的跨平台标准解决方案,但以下是常见实现方式,包括手动处理信号和平台特定机制。以下解释逐步如何实现,并提供代码示例。

1. 通用方式:捕获信号并手动终止子进程(跨平台,适用于正常退出和可捕获信号)

这是一种简单、跨平台的做法:父进程监听退出信号(如 SIGINT、SIGTERM),在收到信号时主动杀死子进程。这种方式适用于父进程正常退出或被可捕获信号终止的情况,但如果父进程被 kill -9 (SIGKILL) 强制杀死,则无效(因为 SIGKILL 无法捕获)。

  • 步骤:

    • 使用 signal.Notify 监听信号。
    • 启动子进程后,记录 PID。
    • 在信号处理中逐个终止子进程(如果需要杀死子进程的子进程,可使用进程组,详见下文)。
  • 代码示例:

    go 复制代码
    package main
    
    import (
        "context"
        "fmt"
        "os"
        "os/exec"
        "os/signal"
        "syscall"
        "time"
    )
    
    func main() {
        // 启动子进程,例如运行 "sleep 60"
        cmd := exec.Command("sleep", "60")
        if err := cmd.Start(); err != nil {
            fmt.Println("Start error:", err)
            return
        }
        fmt.Printf("Child process started with PID: %d\n", cmd.Process.Pid)
    
        // 监听信号
        sigs := make(chan os.Signal, 1)
        signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    
        //  goroutine 处理信号
        go func() {
            sig := <-sigs
            fmt.Printf("Received signal: %v\n", sig)
            if cmd.Process != nil {
                if err := cmd.Process.Kill(); err != nil {
                    fmt.Println("Kill error:", err)
                }
            }
            os.Exit(0)
        }()
    
        // 等待子进程正常结束(可选,如果子进程有自己的退出逻辑)
        if err := cmd.Wait(); err != nil {
            fmt.Println("Wait error:", err)
        }
    }
  • 扩展:使用进程组杀死子进程及其后代(Unix-like 系统) :

    要杀死整个进程树:

    go 复制代码
    cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
    // 在杀死时:
    syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)

2. Linux 特定:使用 Pdeathsig 自动发送信号

在 Linux 上,可以使用 syscall.SysProcAttr.Pdeathsig 设置当父进程(或创建线程)死亡时,内核自动向子进程发送指定信号(如 SIGTERM 或 SIGKILL)。这实现了"自动"终止,即使父进程被 SIGKILL 杀死。

  • 注意:

    • 只适用于 Linux(基于 prctl(PR_SET_PDEATHSIG))。
    • 如果子进程是多线程的,可能有边缘问题(见 Go issue #27505)。
    • 子进程需要能响应信号(例如,如果它忽略 SIGTERM,则用 SIGKILL)。
  • 代码示例:

    go 复制代码
    package main
    
    import (
        "fmt"
        "os/exec"
        "syscall"
    )
    
    func main() {
        cmd := exec.Command("sleep", "60")
        cmd.SysProcAttr = &syscall.SysProcAttr{
            Pdeathsig: syscall.SIGKILL,  // 或 syscall.SIGTERM
        }
        if err := cmd.Start(); err != nil {
            fmt.Println("Start error:", err)
            return
        }
        fmt.Printf("Child process started with PID: %d\n", cmd.Process.Pid)
    
        // 父进程可以继续其他工作,或直接退出测试
        // cmd.Wait()  // 可选
    }

3. Windows 特定:使用 Job Object 自动终止

在 Windows 上,没有直接等价于 Pdeathsig 的机制,但可以使用 Windows Job Object 将进程分组,并设置 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 标志:当 Job Object 的最后一个句柄关闭(父进程退出)时,自动杀死所有关联进程。

  • 注意:

    • Go 标准库不直接支持,需要使用 golang.org/x/sys/windows 包调用 Windows API。
    • 需要安装依赖:go get golang.org/x/sys/windows。
    • 这会终止子进程及其后代。
  • 代码示例(简化版,需要处理错误和清理):

    go 复制代码
    package main
    
    import (
        "fmt"
        "os/exec"
        "syscall"
        "unsafe"
    
        "golang.org/x/sys/windows"
    )
    
    func main() {
        // 创建 Job Object
        job, err := windows.CreateJobObject(nil, nil)
        if err != nil {
            fmt.Println("CreateJobObject error:", err)
            return
        }
        defer windows.CloseHandle(job)
    
        // 设置 Kill on Job Close
        var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
        info.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
        err = windows.SetInformationJobObject(
            job,
            windows.JobObjectExtendedLimitInformation,
            uintptr(unsafe.Pointer(&info)),
            uint32(unsafe.Sizeof(info)),
        )
        if err != nil {
            fmt.Println("SetInformationJobObject error:", err)
            return
        }
    
        // 将当前进程分配到 Job Object(子进程会继承)
        err = windows.AssignProcessToJobObject(job, windows.CurrentProcess())
        if err != nil {
            fmt.Println("AssignProcessToJobObject error:", err)
            return
        }
    
        // 启动子进程
        cmd := exec.Command("timeout", "/t", "60")  // Windows 等价于 sleep
        if err := cmd.Start(); err != nil {
            fmt.Println("Start error:", err)
            return
        }
        fmt.Printf("Child process started with PID: %d\n", cmd.Process.Pid)
    
        // cmd.Wait()  // 可选
    }
  • 说明:子进程启动后会自动关联 Job Object(因为父进程已关联)。父进程退出时,Job Object 关闭,子进程被终止。

其他注意

  • 跨平台实现 :可以使用条件编译(//go:build linux 等)来区分平台特定代码。对于通用场景,优先使用信号捕获方式。
  • 测试 :在 Unix 上用 kill <pid> 测试;在 Windows 上用 Task Manager 杀死父进程。
  • 局限性:没有方式能 100% 保证在所有情况下(如父进程崩溃)自动终止,尤其是跨平台。如果子进程是 daemon 或忽略信号,可能需要额外处理。
相关推荐
geovindu几秒前
rust: search
开发语言·后端·rust
the局外人3 分钟前
读懂LangGraph 的分支执行逻辑
后端·langchain·llm
SimonKing7 分钟前
Stream-Nexus:我的轻量级推送中间件(sse、websocket)终于定下来了
java·后端·程序员
打工仔折腾 AI20 分钟前
网易UU远程实测:手机控电脑做Python开发和Agent调试的真实体验
人工智能·后端·python·智能手机·性能优化·电脑·ai agent 实战
YYYing.31 分钟前
【设计模式系列 (四) 】建造者模式
c++·后端·设计模式·建造者模式·c/c++
IT_陈寒1 小时前
用Vue时这个响应式陷阱坑了我一整天
前端·人工智能·后端
孙启超1 小时前
【AI开发之Rust】第 20 课:壳侧 API 封装 —— 把 Rust 能力接进真实 UI
开发语言·后端·rust
BingoGo1 小时前
用 Jev 与 Laravel AI SDK 检测垃圾邮件和自动回复
后端·laravel
JaguarJack1 小时前
用 Jev 与 Laravel AI SDK 检测垃圾邮件和自动回复
后端·ai·php·laravel·服务端
mldong1 小时前
同一套充血模型,八种语言八种长相
后端·架构