使用os/exec在Golang中终止一个已启动的进程

huangapple go评论81阅读模式
英文:

Terminating a Process Started with os/exec in Golang

问题

有没有办法在Golang中终止使用os.exec启动的进程?例如(来自http://golang.org/pkg/os/exec/#example_Cmd_Start):

cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
    log.Fatal(err)
}
log.Printf("等待命令完成...")
err = cmd.Wait()
log.Printf("命令完成,错误信息:%v", err)

有没有办法在3秒后提前终止该进程?

英文:

Is there a way to terminate a process started with os.exec in Golang? For example (from http://golang.org/pkg/os/exec/#example_Cmd_Start),

cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
    log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)

Is there a way to terminate that process ahead of time, perhaps after 3 seconds?

答案1

得分: 164

运行并终止一个exec.Process

// 启动一个进程:
cmd := exec.Command("sleep", "5")
if err := cmd.Start(); err != nil {
	log.Fatal(err)
}

// 终止进程:
if err := cmd.Process.Kill(); err != nil {
    log.Fatal("failed to kill process: ", err)
}

在超时后运行并终止一个exec.Process

ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()

if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
    // 这将在3秒后失败。5秒的休眠将被中断。
}

Go文档中查看此示例


旧版

在Go 1.7之前,我们没有context包,因此此答案不同。

使用通道和goroutine在超时后运行并终止一个exec.Process

// 启动一个进程:
cmd := exec.Command("sleep", "5")
if err := cmd.Start(); err != nil {
	log.Fatal(err)
}

// 等待进程完成或在超时后终止它(以先发生的为准):
done := make(chan error, 1)
go func() {
	done <- cmd.Wait()
}()
select {
case <-time.After(3 * time.Second):
	if err := cmd.Process.Kill(); err != nil {
		log.Fatal("failed to kill process: ", err)
	}
	log.Println("process killed as timeout reached")
case err := <-done:
	if err != nil {
		log.Fatalf("process finished with error = %v", err)
	}
	log.Print("process finished successfully")
}

进程要么结束并通过done接收其错误(如果有),要么经过3秒后程序被终止。

英文:

Run and terminate an exec.Process:

// Start a process:
cmd := exec.Command(&quot;sleep&quot;, &quot;5&quot;)
if err := cmd.Start(); err != nil {
	log.Fatal(err)
}

// Kill it:
if err := cmd.Process.Kill(); err != nil {
    log.Fatal(&quot;failed to kill process: &quot;, err)
}

Run and terminate an exec.Process after a timeout:

ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()

if err := exec.CommandContext(ctx, &quot;sleep&quot;, &quot;5&quot;).Run(); err != nil {
    // This will fail after 3 seconds. The 5 second sleep
    // will be interrupted.
}

See this example in the Go docs


Legacy

Before Go 1.7, we didn't have the context package and this answer was different.

Run and terminate an exec.Process after a timeout using channels and a goroutine:

// Start a process:
cmd := exec.Command(&quot;sleep&quot;, &quot;5&quot;)
if err := cmd.Start(); err != nil {
	log.Fatal(err)
}

// Wait for the process to finish or kill it after a timeout (whichever happens first):
done := make(chan error, 1)
go func() {
	done &lt;- cmd.Wait()
}()
select {
case &lt;-time.After(3 * time.Second):
	if err := cmd.Process.Kill(); err != nil {
		log.Fatal(&quot;failed to kill process: &quot;, err)
	}
	log.Println(&quot;process killed as timeout reached&quot;)
case err := &lt;-done:
	if err != nil {
		log.Fatalf(&quot;process finished with error = %v&quot;, err)
	}
	log.Print(&quot;process finished successfully&quot;)
}

Either the process ends and its error (if any) is received through done or 3 seconds have passed and the program is killed before it's finished.

答案2

得分: 27

其他答案关于调用Kill()的部分是正确的,但是关于在超时后终止进程的部分现在有点过时了。

现在可以使用context包和exec.CommandContext来实现(示例改编自文档中的示例):

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()

    if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
        // 这将在100毫秒后失败。5秒的睡眠将被中断。
    }
}

从文档中可以看到:

如果在命令完成之前上下文变为完成状态,则使用提供的上下文来终止进程(通过调用os.Process.Kill)。

Run()完成后,您可以检查ctx.Err()。如果达到了超时时间,返回的错误类型将是DeadLineExceeded。如果它是nil,则检查Run()返回的err以查看命令是否完成且无错误。

英文:

The other answers are right about calling Kill(), but the parts regarding killing the process after a timeout are little outdated now.

This can be done now with the context package and exec.CommandContext (example adapted from the example in the docs):

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	if err := exec.CommandContext(ctx, &quot;sleep&quot;, &quot;5&quot;).Run(); err != nil {
		// This will fail after 100 milliseconds. The 5 second sleep
		// will be interrupted.
	}
}

From the docs:
> The provided context is used to kill the process (by calling os.Process.Kill) if the context becomes done before the command completes on its own.

After the Run() completes, you can inspect ctx.Err(). If the timeout was reached, the type of the error returned will be DeadLineExceeded. If it's nil, check the err returned by Run() to see if the command completed without errors.

答案3

得分: 9

一个更简单的版本,没有使用select和通道。

func main() {
    cmd := exec.Command("cat", "/dev/urandom")
    cmd.Start()
    timer := time.AfterFunc(1*time.Second, func() {
        err := cmd.Process.Kill()
        if err != nil {
            panic(err) // 无法杀死进程时抛出panic。
        }
    })
    err := cmd.Wait()
    timer.Stop()

    // 从这里读取错误,你会注意到来自Kill的错误
    fmt.Println(err)
}

好吧,在咨询了一些有经验的Go程序员之后,这显然不是一个足够Go的解决问题的方式。所以请参考被接受的答案。


这是一个更短的版本,非常直接。但是,如果超时时间很长,可能会有大量挂起的goroutine。

func main() {
    cmd := exec.Command("cat", "/dev/urandom")
    cmd.Start()
    go func(){
        time.Sleep(timeout)
        cmd.Process.Kill()
    }()
    return cmd.Wait()
}
英文:

A simpler version without select and channels.

func main() {
	cmd := exec.Command(&quot;cat&quot;, &quot;/dev/urandom&quot;)
    cmd.Start()
	timer := time.AfterFunc(1*time.Second, func() {
		err := cmd.Process.Kill()
		if err != nil {
			panic(err) // panic as can&#39;t kill a process.
		}
	})
	err := cmd.Wait()
    timer.Stop()

    // read error from here, you will notice the kill from the 
    fmt.Println(err)
}

> Well, after consulting some experienced go programmer, this is apparently not a GOly enough way to solve the problem. So please refer to the accepted answer.


Here is an even shorter version, and very straight forward. BUT, possibly having tons of hanging goroutines if timeout is long.

func main() {
    cmd := exec.Command(&quot;cat&quot;, &quot;/dev/urandom&quot;)
	cmd.Start()
	go func(){
		time.Sleep(timeout)
		cmd.Process.Kill()
	}()
	return cmd.Wait()
}

答案4

得分: 6

虽然exec.CommandContext非常方便,在大多数情况下都能正常工作,但我在处理进程的子进程保持活动状态时遇到了一些问题,导致cmd.Wait()挂起。

如果有人遇到类似的情况,以下是我解决问题的方法。

  1. 在启动命令之前,请求创建进程组,使用Setpgid
  2. 启动一个Go协程,在超时时杀死进程组

简单示例(为了可读性):

cmd := exec.Command("sleep", "5")

// 请求操作系统为新进程分配进程组,所有子进程都将属于该进程组
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

go func() {
    time.Sleep(time.Second)
    // 发送杀死信号给进程组而不是单个进程(它的值与PID相同,只是负数)
    syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 
}()

err := cmd.Run()
if err != nil {
    log.Fatal(err)
}
log.Printf("命令成功完成")

更好的示例(对于新手来说可能不太直观):

// 创建一个带有超时的上下文,超时时将关闭ctx.Done()通道
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // 确保上下文被取消,函数退出时将关闭ctx.Done()通道
cmd := exec.Command("sleep", "5")

// 请求操作系统为新进程分配进程组,所有子进程都将属于该进程组
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

go func() {
    // 等待超时或延迟取消
    <-ctx.Done()

    // 发送杀死信号给进程组而不是单个进程(它的值与PID相同,只是负数)
    _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}()

err := cmd.Run()
if err != nil {
    log.Fatal(err)
}
log.Printf("命令成功完成")

附注:为了简洁起见,我用cmd.Run替换了cmd.Start + cmd.Wait

英文:

While exec.CommandContext is very convenient and works fine in most cases, I had some issues with the process' children staying alive - which resulted in cmd.Wait() hanging.

If someone encounters a similar situation, here's how I solved the issue.

  1. Request process group to be created before starting the command using Setpgid
  2. Start a go routine that will kill the process group upon timeout

Naive example (for readability):

cmd := exec.Command(&quot;sleep&quot;, &quot;5&quot;)

// Request the OS to assign process group to the new process, to which all its children will belong
cmd.SysProcAttr = &amp;syscall.SysProcAttr{Setpgid: true}

go func() {
    time.Sleep(time.Second)
    // Send kill signal to the process group instead of single process (it gets the same value as the PID, only negative)
    syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 
}

err := cmd.Run()
if err != nil {
    log.Fatal(err)
}
log.Printf(&quot;Command finished successfully&quot;)

A bit nicer example (can be less intuitive for new Gophers):

	// Create a context with timeout, which will close ctx.Done() channel upon timeout
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel() // Make sure the context is canceled, which will close ctx.Done() channel on function exit
	cmd := exec.Command(&quot;sleep&quot;, &quot;5&quot;)

	// Request the OS to assign process group to the new process, to which all its children will belong
	cmd.SysProcAttr = &amp;syscall.SysProcAttr{Setpgid: true}

	go func() {
		// Wait until timeout or deferred cancellation
		&lt;- ctx.Done()

		// Send kill signal to the process group instead of single process (it gets the same value as the PID, only negative)
		_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
	}()

	err := cmd.Run()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf(&quot;Command finished successfully&quot;)

P.S. For brevity, I replaced cmd.Start + cmd.Wait with cmd.Run

huangapple
  • 本文由 发表于 2012年8月9日 23:14:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/11886531.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定