【问题标题】:how to keep subprocess running after program exit in golang?golang程序退出后如何保持子进程运行?
【发布时间】:2017-02-26 16:50:29
【问题描述】:

我注意到使用Start() 创建的子进程将在程序退出后终止,例如:

package main

import "os/exec"

func main() {
    cmd := exec.Command("sh", "test.sh")
    cmd.Start()
}

main() 退出时,test.sh 将停止运行

【问题讨论】:

  • 似乎存在一个问题,即“当 main() 退出时,test.sh 将停止运行”如何被断言和/或为什么结果不是你所期望的。 test.sh 预计会有多长时间的运行时间?执行 go 程序后多久检查一下test.sh 进程是否仍在运行?采取以下test.shfor _ in $(seq 20); do sleep 1; done。现在,运行 go 程序并立即运行 pgrep -f test.sh。它应该返回一个进程 ID 大约。 19-20 秒。 go 程序退出后。这是cmd.Start() 的预期行为。

标签: go subprocess


【解决方案1】:

子进程应该在你的进程结束后继续运行,只要它干净地结束,如果你点击^C就不会发生这种情况。 您可以做的是拦截发送到您的进程的信号,以便您可以干净地结束。

sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan,
    syscall.SIGINT,
    syscall.SIGKILL,
    syscall.SIGTERM,
    syscall.SIGQUIT)
go func() {
    s := <-sigchan
    // do anything you need to end program cleanly
}()

【讨论】:

  • 是的,现在我知道子进程不会随主程序退出,我将在 shell 命令中添加&amp;,(不知道^C 是否也会杀死子进程
【解决方案2】:

一旦 go 程序完成,子进程(如果没有在 go 程序中等待)将继续运行(除非子进程自然在父 go 程序之前完成)。

原始发布者可能遇到的问题是他们可能提前终止了他们的 go 程序(例如使用&lt;Ctrl-c&gt;),并且由于 go 程序没有干净地退出,它产生的子进程也被终止了。

以下是有助于验证此行为的简化测试用例...

首先我创建一个我想运行的 bash shell 脚本(例如test.sh,不要忘记chmod +x ./test.sh,所以脚本被认为是“可执行的”)。脚本非常简单。它休眠 10 秒,然后创建一个名为 testfile 的新文件(如果它不存在),或者如果该文件已经存在,它将更新“最后修改”时间戳。这很重要,因为这是我在 go 程序完成后确认 bash 脚本仍在运行的方式(由于 10 秒的睡眠,我希望在 bash 脚本完成之前很久就完成)。

#!/usr/local/bin/bash

sleep 10
touch testfile

接下来,我有一个简单的 go 程序,它生成一个运行上述 bash 脚本的子进程,但重要的是不等待它完成。你会看到我还在 Go 程序中添加了 2 秒的睡眠时间,这让我有时间按下 &lt;Ctrl-c&gt;。现在,即使我有 2 秒的睡眠,这个程序(如果让我在没有按 &lt;Ctrl-c&gt; 的情况下运行)将在子进程 bash 脚本执行之前完成(它正在睡眠 10 秒):

package main

import (
    "fmt"
    "log"
    "os/exec"
    "time"
)

func main() {
    cmd := exec.Command("./test.sh")
    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    }
    time.Sleep(2 * time.Second)
    fmt.Println("program finished, but what about the subprocess?")
}

如果我运行 go 程序并让它自然完成,我可以ls -l testfile 并检查它的时间戳。然后,我将等待 10 秒并再次运行 ls -l testfile,我将看到时间戳更新(显示子进程已成功完成)。

现在如果我要重新运行 go 程序,这次在程序完成之前按&lt;Ctrl-c&gt;(这就是我添加 2 秒睡眠的原因),那么不仅 go 程序会提前退出,而且子进程也会提前退出也将终止。所以我可以等待 10 秒或 10 小时或更长时间,没关系。 testfile 上的时间戳不会更新,证明子进程已终止。

【讨论】:

  • 你错过了问题的重点。仍然信息丰富:)
【解决方案3】:

尝试修改您的程序以使用运行而不是启动。这样,Go 程序将等待 sh 脚本完成后再退出。

package main

import (
    "log"
    "os/exec"
)

func main() {
    cmd := exec.Command("sh", "test.sh")
    err := cmd.Run()
    if err != nil {
        log.Fatalln(err)
    }
}

同样,您始终可以使用等待组,但我认为这有点矫枉过正。

您也可以只是一个带或不带等待组的 goroutine。取决于你是否要等待 sh 程序完成

package main

import (
    "os/exec"
)

func runOffMainProgram() {
    cmd := exec.Command("sh", "test.sh")
    cmd.Start()
}

func main() {
    // This will start a go routine, but without a waitgroup this program will exit as soon as it runs
    // regardless the sh program will be running in the background. Until the sh program completes
    go runOffMainProgram()
}

【讨论】:

  • 感谢您的回答,但我希望 test.sh 即使在主程序死机时也能继续运行,在这种情况下,子进程完全独立于 go 程序
  • 什么意思?您是否正在杀死主程序。在上面的示例中,主程序在 sh 程序完成之前不会死亡,同样您正在创建上面的子进程。如果你运行上面的程序,然后使用类似 ps aux |在终端中运行 grep test.sh,您会看到脚本独立于 go 程序运行。
  • 如果我将&amp; 添加到我的命令中会怎样?...我认为这可能是一个解决方案
  • 只是想确保子进程始终在运行,即使主程序不在,是的,Run()Wait() 会告诉 Golang 等待我的进程完成,但我没有不需要那个
  • 这不会改变crtl-c 的处理方式。看我的回答。但是,在 shell 上运行带有 & 符号的主程序(发送到后台)确实会有所不同!
【解决方案4】:

接受的答案对于信号应该在哪里处理是模糊的。我认为如果可能的话,必须使用一些更复杂的技术来防止向孩子发送中断。

TLDR;

因此,处理ctrl-c 的唯一方法是预测 SIGINT 并在子级中处理该信号。


我自己做了一些实验。

go build -o ctrl-c ctrl-c.go

如果程序被发送到后台,杀死主进程的唯一方法是使用kill -9 (SIGKILL)。

SIGTERM (15) 不行。

$ ./ctrl-c & cmd=$! ; sleep 1 && echo kill $cmd && kill $cmd 
[1] 1165918
1165918
bashed 1165926
bashed 1165927
bashed 1165928
main()
go SIGN 23 urgent I/O condition
go SIGN 23 urgent I/O condition
main()
kill 1165918
go SIGN 15 terminated
main()
$ main()
main()
main()
main()
main()
main() done.
Bash _ 1165926 EXITs
Bash q 1165927 EXITs
Bash c 1165928 EXITs

[1]+  Done                    ./ctrl-c

SIGINT (2) 不行。

$ ./ctrl-c & cmd=$! ; sleep 1 && echo kill $cmd &&  kill -INT  $cmd 
[1] 1167675
1167675
bashed 1167683
bashed 1167684
bashed 1167685
main()
main()
kill 1167675
go SIGN 2 interrupt
main()
balmora: ~/src/my/go/doodles/sub-process [master]
$ main()
main()
main()
main()
main()
main() done.
Bash _ 1167683 EXITs
Bash q 1167684 EXITs
Bash c 1167685 EXITs

SIGKILL 会杀死主进程,但不会杀死 bash 子命令。


$ ./ctrl-c & cmd=$! ; sleep 1 && echo kill $cmd &&  kill -KILL  $cmd 
[1] 1170721
1170721
bashed 1170729
bashed 1170730
bashed 1170731
main()
main()
kill 1170721
[1]+  Killed                  ./ctrl-c

Bash _ 1170729 EXITs
Bash q 1170730 EXITs
Bash c 1170731 EXITs

但是,如果 go 二进制文件在前台运行,那么只有处理 SIGINT 的子进程才会继续运行。这感觉与上述发现几乎相反

$ ./ctrl-c 
1186531
bashed 1186538
bashed 1186539
bashed 1186540
main()
main()
main()
main()
main()
main()
^C

Bash c 1186540 INTs quit
Bash q 1186539 INTs ignored

Bash c 1186540 EXITs

Bash _ 1186538 INTs ignored
go SIGN 2 interrupt
go SIGN 17 child exited
6q ELAPSED 2

Bash q 1186539 EXITs
6_ ELAPSED 2

Bash _ 1186538 EXITs
go SIGN 17 child exited
main()
main()
main() done.

无论如何,对我来说,当使用Cmd.Start() 时,ctrl+c 被转发给孩子们。如果使用Cmd.Run(),则行为相同,但Cmd.Run() 将在每个子命令退出之前等待。在 go 例程 (go func(){}()) 中运行 Cmd 不会改变任何内容。如果子命令作为 go-routine 或使用 Cmd.Start()“并行”启动,则中断信号将同时到达所有子命令。

为了在中断后保持子命令在交互式终端上运行,我认为子命令必须处理信号并忽略它。


我试验的代码:


package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

func signs(s ...os.Signal) chan os.Signal {
    signals := make(chan os.Signal, 1)
    signal.Notify(signals, s...)
    signal.Notify(signals,
        os.Interrupt, syscall.SIGINT, syscall.SIGQUIT, // keyboard
        syscall.SIGKILL, syscall.SIGHUP, syscall.SIGTERM, // os termination
        syscall.SIGUSR1, syscall.SIGUSR2, // user
        syscall.SIGPIPE, syscall.SIGCHLD, syscall.SIGSEGV, // os other
    )
    return signals
}

func interpret(signals chan os.Signal) chan os.Signal {
    go func() {
        for ;; {
            select {
            case sign := <-signals:
                elog("go SIGN %#v %s", sign, sign)
            }
        }
    }()
    return signals
}

func bash(script string) {
    cmd := exec.Command("/bin/bash", "-c", script )
    cmd.Stdout = os.Stderr
    err := cmd.Start()
    //err := cmd.Run()
    if err != nil {
        log.Fatal(err)
    }
    elog("bashed %d", cmd.Process.Pid)
}

func main() {
    fmt.Println(os.Getpid())

    signals := interpret(signs())
    signals = signals

    //go bash(`
    bash(`
        trap ' echo Bash _ $$  INTs ignored; ' SIGINT
        trap ' echo Bash _ $$ QUITs ignored; ' SIGQUIT
        trap ' echo Bash _ $$ EXITs'           EXIT
        sleep 6;
        echo 6_ $( ps -o etimes -p $$ )

        #for i in {1..60}; do echo -n _; sleep 0.1; done; echo
    `)

    // go bash(`
    bash(`
        trap ' echo Bash q $$  INTs ignored; ' SIGINT
        trap ' echo Bash q $$ QUITs; exit    ' SIGQUIT
        trap ' echo Bash q $$ EXITs;         ' EXIT
        sleep 6;
        echo 6q $( ps -o etimes -p $$ )
        #for i in {1..60}; do echo -n q; sleep 0.1; done; echo
    `)

    //go bash(`
    bash(`
        trap ' echo Bash c $$  INTs quit; exit   ' SIGINT
        trap ' echo Bash c $$ QUITs ignored; ' SIGQUIT
        trap ' echo Bash c $$ EXITs'           EXIT
        sleep 6;
        echo 6c $( ps -o etimes -p $$ )
        #for i in {1..60}; do echo -n c; sleep 0.1; done; echo
    `)

    go func() {
        for ;; {
            time.Sleep(time.Millisecond * 333)
            elog("main()")
        }
    }()

    time.Sleep(3 * time.Second)
    elog("main() done.")
}

func echo(a ...interface{}) {
    _, err := fmt.Println(a...)
    if err != nil {
        fmt.Println("ERR ", err.Error())
    }
}

func elog(form string, arg ...interface{}) {
    println(fmt.Sprintf(form, arg...))
}


【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-03
    • 1970-01-01
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    • 2013-03-03
    相关资源
    最近更新 更多