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 或忽略信号,可能需要额外处理。
相关推荐
李迟几秒前
Golang实践录:工程框架实践:搭建工程模板
开发语言·后端·golang
Lynko6 分钟前
C语言指针高阶
后端
the局外人6 分钟前
学习 FastAPI 的 Day 2:用异步 ORM 完成增删改查
后端·python·fastapi
SamDeepThinking11 分钟前
if嵌套最好控制在3层以内
java·后端·程序员
苏渡苇14 分钟前
Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate
java·后端·spring·spring cloud·springboot·性能监控
tonydf15 分钟前
AI驱动历史项目安全审查
后端·ai编程
她的男孩31 分钟前
同一个接口,为什么销售只看到自己的订单?我拆了 681 行数据权限拦截器的源码
java·后端·架构
元界metalite33 分钟前
SpringBoot接口通用响应对象设计 - 一个 Resp 管理五种责任
后端
对象存储与RustFS36 分钟前
把 RustFS 跑在 Kubernetes 上:官方 Helm Chart 从单机到分布式
后端·rust·开源
SimonKing1 小时前
将cURL 命令直接变成 Java 代码?用JQuick-Curl搞起来
java·后端·程序员