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 或忽略信号,可能需要额外处理。
相关推荐
青柠编程28 分钟前
基于 Spring Boot 的医疗病历信息交互平台架构设计
java·spring boot·后端
chenyuhao20242 小时前
vector深度求索(上)实用篇
开发语言·数据结构·c++·后端·算法·类和对象
程序新视界2 小时前
MySQL中的数据去重,该用DISTINCT还是GROUP BY?
数据库·后端·mysql
豌豆花下猫3 小时前
Python 潮流周刊#121:工程师如何做出高效决策?
后端·python·ai
懒惰蜗牛4 小时前
Day24 | Java泛型通配符与边界解析
java·后端·java-ee
Eoch774 小时前
从买菜到秒杀:Redis为什么能让你的网站快如闪电?
java·后端
我不是混子4 小时前
奇葩面试题:线程调用两次start方法会怎样?
java·后端
摸鱼总工5 小时前
为什么读源码总迷路?有破解办法吗
后端
仙俊红5 小时前
深入理解 ThreadLocal —— 在 Spring Boot 中的应用与原理
java·spring boot·后端
折七5 小时前
告别传统开发痛点:AI 驱动的现代化企业级模板 Clhoria
前端·后端·node.js