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 或忽略信号,可能需要额外处理。
相关推荐
花花无缺3 小时前
python自动化-pytest-用例发现规则和要求
后端·python
程序员小假3 小时前
我们来说一说 Cglib 与 JDK 动态代理
后端
摆烂工程师5 小时前
教你如何认证 Gemini 教育优惠的二次验证,薅个 1年的 Gemini Pro 会员
后端·程序员·gemini
绝无仅有5 小时前
未来教育行业的 Go 服务开发解决方案与实践
后端·面试·github
程序员爱钓鱼5 小时前
Go语言实战案例- 命令行参数解析器
后端·google·go
心在飞扬5 小时前
Redis 介绍与 Node.js 使用教程
后端
milanyangbo6 小时前
“卧槽,系统又崩了!”——别慌,这也许是你看过最通俗易懂的分布式入门
分布式·后端·云原生·架构
AAA修煤气灶刘哥6 小时前
MySQL 查文本查哭了?来唠唠 ES 这货:从 “啥是 ES” 到 Java 撸代码,一篇整明白!
java·后端·elasticsearch
金銀銅鐵6 小时前
[Java] 浅析密封类(Sealed Classes) 在 class 文件中是如何实现的
java·后端