【问题标题】:Why won't Go kill a child process correctly?为什么 Go 不能正确杀死子进程?
【发布时间】:2014-03-18 04:08:27
【问题描述】:

当 cmd 在分配的时间内完成时,以下工作正常。但是,超时不起作用。虽然它确实打印了"It's dead Jim",但它不仅无法打印"Done waiting",而且该进程实际上并没有被杀死。它继续运行,"Done waiting" 永远不会打印。

func() {
    var output bytes.Buffer
    cmd := exec.Command("Command", args...)
    cmd.Dir = filepath.Dir(srcFile)
    cmd.Stdout, cmd.Stderr = &output, &output
    if err := cmd.Start(); err != nil {
        return err
    }
    defer time.AfterFunc(time.Second*2, func() {
        fmt.Printf("Nobody got time fo that\n")
        if err := cmd.Process.Signal(syscall.SIGKILL); err != nil {
            fmt.Printf("Error:%s\n", err)
        }
        fmt.Printf("It's dead Jim\n")
    }).Stop()
    err := cmd.Wait()
    fmt.Printf("Done waiting\n")
}()

我不认为它应该有所作为,但对于它的价值来说,命令是go test html。它超时的原因是因为我在运行它之前注入了一个导致无限循环的错误。为了增加混乱,我尝试使用go test net 运行它。有一个超时,它工作正常。

【问题讨论】:

  • 一个原因可能是Kill() 只向进程发送TERM 信号,进程可以随意忽略或特殊处理。如果目标进程被阻塞(例如,它当前处于不可重新启动的系统调用中),也可能不会传递信号。可以肯定的是,让Kill() 发送KILL 信号。
  • @kostix,感谢您指出这一点。 documentation 相当含糊。我现在明确发送 SIGKILL,但它仍然无法正常工作。
  • 因为SIGKILL 肯定会终止进程(除非它以某种方式严重楔入)我倾向于认为还有其他问题。在您的真实代码中,您是否检查对os.Process.Kill() 的调用是否有错误?
  • 从 Go 1.7 开始,为孩子提供取消或超时的首选方式是使用 exec.CommandContext 并安排取消或超时上下文。
  • Kill() 发送 SIGKILL,不能被捕获或忽略。见this answer。在上下文中调用取消也会发送 SIGKILL。

标签: go exec


【解决方案1】:

我不确定它是何时添加的,但从 Go 1.11 开始,您可以将子进程上的 Pdeathsig 设置为 syscall.SIGKILL。当父母退出时,这将杀死孩子。

cmd, _ := exec.Command("long-running command")
cmd.SysProcAttr = &syscall.SysProcAttr{
    Pdeathsig: syscall.SIGKILL,
}
cmd.Start()

os.Exit(1)

cmd 应该在退出时被杀死。

【讨论】:

    【解决方案2】:

    看起来问题在于 cmd.Process.Kill() 不会杀死子进程。看到这个类似的问题Process.Kill() on child processes

    我在这个帖子https://groups.google.com/forum/#!topic/golang-nuts/XoQ3RhFBJl8找到了解决方案

    cmd := exec.Command( some_command )
    cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
    cmd.Start()
    
    pgid, err := syscall.Getpgid(cmd.Process.Pid)
    if err == nil {
        syscall.Kill(-pgid, 15)  // note the minus sign
    }
    
    cmd.Wait()
    

    作为一个警告,这几乎肯定不会跨平台工作 - 我目前在 OSX Yosemite 上,我愿意打赌它也可以在大多数 Linux 上工作,但我知道的还不够多关于 BSD 有意见,我怀疑它是否适用于 Windows。

    【讨论】:

    • 我在优胜美地,不幸的是它似乎并没有杀死所有的子进程。我正在执行 GAE sdk,但它无法杀死所有子进程(即 http 端口保持打开状态,所以我猜一些子进程仍在运行)
    • 对我来说,只有子进程被杀死,使用这个解决方案。但是用简单的cmd.Process.Kill() 替换syscall.Getpgid()syscall.Kill()确实 杀死了父进程及其所有子进程。供参考。 (我仍然使用Setpgid: true。)
    【解决方案3】:

    仅供参考,我也将我的 Windows 解决方案放在这里:

    func kill(cmd *exec.Cmd) error {
        kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
        kill.Stderr = os.Stderr
        kill.Stdout = os.Stdout
        return kill.Run()
     }
    

    【讨论】:

      【解决方案4】:

      Go 的 defer 语句安排函数调用(延迟函数)在执行 defer 的函数返回之前立即运行。

      那么defer之后的事情

      defer time.AfterFunc(time.Second*2, func() {
          fmt.Printf("Nobody got time fo that\n")
          cmd.Process.Kill()
          fmt.Printf("It's dead Jim\n")
      }).Stop()
      

      除非 func() 结束,否则不会执行。因此,如果“cmd.Wait()”永远不会结束,那么“time.AfterFunc()”就永远不会执行。

      从 defer 中删除 "time.AfterFunc(...)" 可以解决这个问题,因为 "time.AfterFunc" 可以等待持续时间过去,然后在其自己的 goroutine 中调用 f

      这是一个工作版本。我在我的 ubuntu 盒子中进行了测试,它可以工作。 将源另存为wait.go

      package main
      
      import "os/exec"
      import "time"
      import "bytes"
      import "fmt"
      
      
      func main() {
          var output bytes.Buffer
              cmd := exec.Command("sleep", "10s")
              cmd.Stdout, cmd.Stderr = &output, &output
              if err := cmd.Start(); err != nil {
                      fmt.Printf("command start error\n")
                      return
              }
              time.AfterFunc(time.Second*2, func() {
                      fmt.Printf("Nobody got time for that\n")
                      cmd.Process.Kill()
                      fmt.Printf("It's dead Jim\n")
              })
              cmd.Wait()
              fmt.Printf("Done waiting\n")
      }
      

      运行命令:

      time go run wait.go
      

      输出:

      Nobody got time for that
      It's dead Jim
      Done waiting
      
      real    0m2.481s
      user    0m0.252s
      sys 0m0.452s
      

      正如@James Henstridge 所说,上述理解是不正确的。其实我对defer的理解不完全。另一半是“延迟函数的参数(如果函数是方法,则包括接收器)在延迟执行时进行评估”。所以在执行 defer 时真正创建了计时器,因此计时器将超时。

      问题是为什么进程不能被杀死。我检查了 go 的 pkg 代码,它在 *nix 之类的系统中发送 SIGKILL 来终止进程。 SIGKILL 不能被阻止和忽略。所以它可能是其他可能性,例如进程本身处于TASK_UNINTERRUPTIBLE 状态。

      【讨论】:

      • 你确定吗?被推迟的是time.AfterFunc 返回的*Timer 上的Stop 调用。例如。 play.golang.org/p/p1vuHNQWxK
      • 感谢您指出这一点。我对延迟的理解不完全。 “延迟函数的参数(如果函数是方法,则包括接收器)在延迟执行时进行评估”。所以在执行 defer 时真正创建了计时器,因此计时器将超时。 @kostix 指出了 kill 不起作用的潜在原因。
      • @jeffruan 我认为该进程不可能处于 TASK_UNINTERRUPTIBLE 状态,因为它的内存占用量继续增长。
      • @Floegipoky,你试过 ps 或 top 来检查它的状态吗? D是不间断状态。或者当 go 代码的 kill 执行失败时,您是否尝试通过运行 shell kill 命令手动终止进程。这只是为了知道是否有区别。
      • @jeffruan,进程状态为R。运行 kill <pid> 确实会成功终止进程。
      【解决方案5】:

      您的调用进程可以使用 setsid 在 posix 系统上创建新会话。当您执行以下操作时,您的代码将成为流程组负责人(如果不是)。当您杀死进程组负责人时,孩子们也会死去。至少,这是我的经验。

      cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
      cmd.Start()
      time.Sleep(5)
      if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil {
              log.Println("failed to kill: ", err)
      }
      

      【讨论】:

      • 请编辑更多信息。不鼓励使用纯代码和“试试这个”的答案,因为它们不包含可搜索的内容,也没有解释为什么有人应该“试试这个”。我们在这里努力成为知识的资源。
      • 不知道为什么这有 0 票,但这对我有用,并且比 setpgid 解决方案稍微简单。
      猜你喜欢
      • 2020-01-10
      • 2014-12-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多