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 或忽略信号,可能需要额外处理。
相关推荐
Victor3562 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易2 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧2 小时前
Range循环和切片
前端·后端·学习·golang
WizLC2 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3562 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法2 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长3 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈3 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao3 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang
壹方秘境3 小时前
一款方便Java开发者在IDEA中抓包分析调试接口的插件
后端