Golang exec.Cmd在Kill后不能关闭进程

Golang本版:1.22.2

出现的场景,在golang项目中需要管理shell脚本的执行,主要实现关闭进程的代码如下:

func (s *Streamer) Kill() error {
    s.mu.Lock()
    defer s.mu.Unlock()
    // 首先关闭输出流
    if closer, ok := s.Cmd.Stdout.(io.Closer); ok {
        if err := closer.Close(); err != nil {
            log.Printf("Failed to close stdout: %v", err)
        }
    }
    if closer, ok := s.Cmd.Stderr.(io.Closer); ok {
        if err := closer.Close(); err != nil {
            log.Printf("Failed to close stderr: %v", err)
        }
    }
    if s.Cmd.Process != nil {
        // Kill the process
        if err := s.Cmd.Process.Kill(); err != nil {
            return err
        }
    }
    // 等待进程关闭
    return s.Cmd.Wait()
}

在前端调用接口触发方法Kill时发现时不时出现接口请求超时,一开始以为是网络的问题,后面经过过排查,发现Cmd.Wait()一直没有结束,导致接口请求超时。在查找资料的过程中发现,Cmd.Wait()会一直等待进程结束,如果进程一直没有结束,那么Cmd.Wait()也会一直等待,然而实际情况是shell已经执行完毕,经过排查是产生了僵尸子进程。

为了解决这个问题,对代码做了一些修改,首先启动一个 goroutine 来处理超时,然后Cmd.Wait()不能无限制等待,可以通过time.After设置一个超时时间,如果超过这个时间还没有结束,将通过发送 SIGTERM 信号,具体实现如下:

func (s *Streamer) Stop() error {
    s.mu.Lock()
    defer s.mu.Unlock()
    // ...
    // 设置超时时间
    timeout := time.Duration(5 * time.Second)
    done := make(chan error, 1)
    // 启动一个 goroutine 来处理超时
    go func() {
        err := s.Cmd.Wait()
        done <- err
    }()
    // 等待命令完成或超时
    select {
    case err := <-done:
        if err != nil {
            fmt.Println("命令执行失败:", err)
        } else {
            fmt.Println("命令执行成功")
        }
    case <-time.After(timeout):
        // 发送 SIGTERM 信号
        if err := exec.Command("kill", "-SIGTERM", fmt.Sprintf("-%d", s.Cmd.Process.Pid)).Run(); err != nil {
            fmt.Println("发送 SIGTERM 信号失败:", err)
        }
        fmt.Println("命令超时,已终止")
    }
    return nil
}

以上代码可能并不完善,仅提供一个思路,具体实现还需要根据实际情况进行调整。