how to keep subprocess running after program exit in golang?

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

how to keep subprocess running after program exit in golang?

问题

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

package main

import "os/exec"

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

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

英文:

i noticed that subprocesses created using Start() will be terminated after program exit, for example:
<!-- language: go -->

package main

import &quot;os/exec&quot;

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

when main() exits, test.sh will stop running

答案1

得分: 4

子进程应该在你的进程结束后继续运行,只要它以干净的方式结束,如果你按下 ^C,这种情况就不会发生。
你可以拦截发送给你的进程的信号,这样你就可以干净地结束它。

sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan,
    syscall.SIGINT,
    syscall.SIGKILL,
    syscall.SIGTERM,
    syscall.SIGQUIT)
go func() {
    s := <-sigchan
    // 做任何你需要的操作来干净地结束程序
}()
英文:

The subprocess should continue to run after your process ends, as long as it ends cleanly, which won't happen if you hit ^C.
What you can do is intercept the signals sent to your process so you can end cleanly.

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

答案2

得分: 3

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

原帖的问题可能是原帖作者可能提前终止了go程序(例如使用<Ctrl-c>),因为go程序没有正常退出,所以它生成的子进程也被终止了。

下面是一个简化的测试案例,用于验证这种行为...

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

#!/usr/local/bin/bash

sleep 10
touch testfile

接下来,我有一个简单的go程序,它生成一个子进程来运行上面的bash脚本,但重要的是它不等待子进程完成。你会看到我在我的go程序中还添加了一个2秒的睡眠,这给我一些时间来按下<Ctrl-c>。现在,即使我有2秒的睡眠,如果我不按<Ctrl-c>让这个程序自然运行,它将在子进程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("程序已完成,但子进程呢?")
}

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

现在,如果我重新运行go程序,这次在程序完成之前按下<Ctrl-c>(这就是为什么我添加了2秒的睡眠),那么不仅go程序会提前退出,子进程也会被终止。所以我可以等待10秒、10小时或更长时间,都没有关系。testfile的时间戳不会更新,证明子进程被终止了。

英文:

A subprocess (if not waited on within the go program) will continue to run once the go program has finished (unless the subprocess naturally finishes before the parent go program).

The problem the original poster is likely encountering is that they are probably terminating their go program early (e.g. using &lt;Ctrl-c&gt;), and because the go program is not exiting cleanly the subprocess it spawned is also terminated.

Below is a reduced test case that helps validate this behaviour...

First I create a bash shell script I want to run (e.g. test.sh, don't forget to chmod +x ./test.sh so the script is considered 'executable'). The script is very simple. It sleeps for 10 seconds and then either creates a new file called testfile (if it doesn't exist) or if the file already exists it will update the 'last modified' timestamp. This is important because this is how I confirm the bash script is still running once my go program finishes (which I expect to finish long before the bash script finishes due to the 10 second sleep).

#!/usr/local/bin/bash

sleep 10
touch testfile

Next, I have a simple go program, which spawns a subprocess that runs the bash script above but importantly doesn't wait for it to complete. You'll see I've also added a 2 second sleep to my go program which gives me some time to press &lt;Ctrl-c&gt;. Now, even though I have a 2 second sleep, this program (if left to run without me pressing &lt;Ctrl-c&gt;) will finish before the subprocess bash script does (which is sleeping for 10 seconds):

package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;os/exec&quot;
	&quot;time&quot;
)

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

If I run the go program and just let it finish naturally, I can ls -l testfile and check the timestamp on it. I'll then wait 10 seconds and run the ls -l testfile again and I will see the timestamp update (which shows the subprocess finished successfully).

Now if I was to re-run the go program and this time press &lt;Ctrl-c&gt; before the program finishes (this is why I add the 2 second sleep), then not only will the go program exit early, but the subprocess will be terminated also. So I can wait 10 seconds or 10 hours or longer, doesn't matter. The timestamp on the testfile will not update, proving the subprocess was terminated.

答案3

得分: 0

尝试修改你的程序,使用Run而不是Start。这样,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)
	}
}

同样,你也可以使用wait group,但我认为在这里使用它有点过度。

你还可以使用带有或不带有wait groupgo routine。这取决于你是否希望Go等待sh程序完成。

package main

import (
	"os/exec"
)

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

func main() {
	// 这将启动一个go routine,但是如果没有waitgroup,该程序将在运行后立即退出,
	// 不管sh程序是否在后台运行。直到sh程序完成。
	go runOffMainProgram()
}
英文:

Try modding you program a to use Run instead of start. In that way the Go program will wait for the sh script to finish before exiting.

package main

import (
	&quot;log&quot;
	&quot;os/exec&quot;
)

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

Likewise, you could always use a wait group but I think that's overkill here.

You could also just a go routine with or without a wait group. Depends on if you want go to wait for the program the sh program to complete

package main

import (
	&quot;os/exec&quot;
)

func runOffMainProgram() {
	cmd := exec.Command(&quot;sh&quot;, &quot;test.sh&quot;)
	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()
}

答案4

得分: 0

接受的答案对于信号处理的位置模糊不清。我认为必须使用一些更复杂的技术来防止将中断发送给子进程,如果可能的话。

简而言之,处理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例程中运行Cmd(go func(){}())不会改变任何事情。如果子命令以“并行”的方式启动,作为go例程或使用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...))
}

以上是要翻译的内容。

英文:

The accepted answer is vague about where the signal should be handled. I think some more sophisticated techniques must be used to prevent sending interrupts to children, if at all possible.

TLDR;

So the only way to deal with ctrl-c is to anticipate the SIGINT and process that signal in the children.


I did some experimentation of my own.

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

If the program is sent to the background, The only way to kill the main process is with kill -9 (SIGKILL).

SIGTERM (15) will not do.

$ ./ctrl-c &amp; cmd=$! ; sleep 1 &amp;&amp; echo kill $cmd &amp;&amp; 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) will not do.

$ ./ctrl-c &amp; cmd=$! ; sleep 1 &amp;&amp; echo kill $cmd &amp;&amp;  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 kills the main process but not the bash sub-commands.


$ ./ctrl-c &amp; cmd=$! ; sleep 1 &amp;&amp; echo kill $cmd &amp;&amp;  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

However, if the go binary is running in the foreground then only children who do deal with SIGINT will be kept running. This feels like almost the opposite of the above findings

$ ./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.

Anyway, the takeaway for me is that <kbd>ctrl</kbd>+<kbd>c</kbd> is forwarded to children when Cmd.Start() is used. The behavior is the same if Cmd.Run() is used, but Cmd.Run() will wait before each sub-command exits. Running the Cmd in a go routine (go func(){}()) does not change anything. If the sub-commands are started "in parallel" as a go-routine or with Cmd.Start(), the the interrupt signal will reach all of them at the same time.

To keep the sub-commands running on an interactive terminal after an interrupt, I think the sub-commands have to handle the signal and ignore it.


The code I experimented with:


package main

import (
    &quot;fmt&quot;
    &quot;log&quot;
    &quot;os&quot;
    &quot;os/exec&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;
    &quot;time&quot;
)

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 := &lt;-signals:
                elog(&quot;go SIGN %#v %s&quot;, sign, sign)
            }
        }
    }()
    return signals
}

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

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

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

    //go bash(`
    bash(`
        trap &#39; echo Bash _ $$  INTs ignored; &#39; SIGINT
        trap &#39; echo Bash _ $$ QUITs ignored; &#39; SIGQUIT
        trap &#39; echo Bash _ $$ EXITs&#39;           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 &#39; echo Bash q $$  INTs ignored; &#39; SIGINT
        trap &#39; echo Bash q $$ QUITs; exit    &#39; SIGQUIT
        trap &#39; echo Bash q $$ EXITs;         &#39; 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 &#39; echo Bash c $$  INTs quit; exit   &#39; SIGINT
        trap &#39; echo Bash c $$ QUITs ignored; &#39; SIGQUIT
        trap &#39; echo Bash c $$ EXITs&#39;           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(&quot;main()&quot;)
        }
    }()

    time.Sleep(3 * time.Second)
    elog(&quot;main() done.&quot;)
}

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

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


huangapple
  • 本文由 发表于 2017年2月27日 00:50:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/42471349.html
匿名

发表评论

匿名网友

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

确定