相关文章推荐
奔跑的小虾米  ·  WPF ...·  2 月前    · 
有腹肌的警车  ·  django ...·  1 年前    · 
儒雅的番茄  ·  flutter handshake ...·  1 年前    · 

当我的shell脚本退出时,如何杀死后台进程/工作?

259 人关注

我正在寻找一种方法来清理我的顶层脚本退出时的混乱。

特别是当我想使用 set -e 时,我希望后台进程能在脚本退出时死亡。

3 个评论
@DanielKaplan 尝试一下,比如 p=$(bash -c 'sleep 2 >/dev/null & echo $!'); sleep 1; ps -f -p "$p" ,看看 sleep 2 命令在 bash 退出后是否还在运行。
@DanielKaplan sleep 2 命令作为一个单独的进程在后台运行;其命令以 & 结束。
@jarno 道歉。我的第一条评论是不正确的,所以我已经删除了我的其他评论。
shell
elmarco
elmarco
发布于 2008-12-12
14 个回答
tokland
tokland
发布于 2019-02-08
已采纳
0 人赞同

这对我来说是有效的(改进后感谢评论者)。

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$ sends aSIGTERM到整个进程组,因此也会杀死后代。

  • 指定信号EXIT在使用set -e时很有用(更多详情here).

  • 总的来说应该工作得不错,但子进程可能会改变进程组。 另一方面,它不需要作业控制,而且还可能得到一些被其他解决方案遗漏的孙子进程。
    I don't quite understand -$$ . It evaluates to '-<PID>` eg -1234 . In the kill manpage // builtin manpage a leading dash specifies the signal to be sent. However -- probably blocks that, but then the leading dash is undocumented otherwise. Any help?
    @EvanBenn:查看 man 2 kill ,它解释了当一个PID为负数时,信号会被发送到进程组中的所有进程,并提供ID( en.wikipedia.org/wiki/Process_group ). 令人困惑的是,在 man 1 kill man bash 中没有提到这一点,可以认为是文档中的一个错误。
    为什么我们在这里有两个嵌套的陷阱?
    @MohammedNoureldin 内部的 trap - SIGTERM 会将当前脚本的SIGTERM响应重置为默认的kill行为。然后,当 kill -- -$$ 被执行时,当前脚本将收到SIGTERM并正常退出。
    Johannes Schaub - litb
    Johannes Schaub - litb
    发布于 2019-02-08
    0 人赞同

    为了清理一些混乱,可以使用 trap 。它可以提供一个当特定信号到达时执行的东西的列表。

    trap "echo hello" SIGINT
    

    但也可以用来在shell退出时执行一些东西。

    trap "killall background" EXIT
    

    这是一个内建程序,所以help trap会给你信息(与bash一起工作)。如果你只想杀死后台工作,你可以做

    trap 'kill $(jobs -p)' EXIT
    

    注意使用单个',以防止外壳立即替代$()

    那么你是如何杀死所有 child 只有?(或者我错过了一些明显的东西)。
    orip
    killall杀了你的孩子,但没有杀你
    kill $(jobs -p) 在dash中不起作用,因为它在子壳中执行命令替换(见man dash中的命令替换)。
    替换代码0】是否应该是一个占位符? background 不在手册页中...
    Alek
    替换代码0】很好,但是在没有后台工作的时候,会打印出'kill'的使用信息。我认为,对bash来说,最好的方法是 jobs -p | xargs -r kill
    korkman
    korkman
    发布于 2019-02-08
    0 人赞同

    更新。 https://stackoverflow.com/a/53714583/302079 通过增加退出状态和清理函数来改进这一点。

    trap "exit" INT TERM
    trap "kill 0" EXIT
    

    为什么将INTTERM转换为退出?因为两者都应该触发kill 0而不进入无限循环。

    为什么要在EXIT上触发kill 0?因为正常的脚本退出也应该触发kill 0

    为什么是kill 0?因为嵌套的子壳也需要被杀死。这将拿下the whole process tree.

    对于我在Debian上的情况来说,这是唯一的解决办法。
    无论是Johannes Schaub的答案还是tokland提供的答案,都没有设法杀死我的shell脚本启动的后台进程(在Debian上)。这个解决方案成功了。我不知道为什么这个答案没有得到更多的支持。你能不能进一步说明一下 kill 0 到底是什么意思/作用?
    这真是太棒了,但也扼杀了我的母体外壳 :-(
    这个解决方案简直是矫枉过正。杀戮0(在我的脚本里面)毁了我的整个X会话也许在某些情况下kill 0是有用的,但这并不能改变它不是通用解决方案的事实,除非有非常好的理由使用它,否则应该尽量避免。如果能加上一个警告,说明它可能会杀死父级shell甚至整个X会话,而不仅仅是脚本的后台工作,那就更好了。
    虽然在某些情况下这可能是一个有趣的解决方案,但正如@vidstige所指出的,这将扼杀了 whole process group 其中包括启动进程(即大多数情况下的父壳)。当你通过IDE运行一个脚本时,这绝对不是你想要的东西。
    skozin
    skozin
    发布于 2019-02-08
    0 人赞同

    The trap 'kill 0' SIGINT SIGTERM EXIT solution described in @tokland的回答 真的很好,但最新的Bash 崩溃时出现分段故障 当使用它时。这是因为Bash,从4.3版开始,允许陷阱递归,在这种情况下,陷阱递归会变成无限的。

  • shell process receives SIGINT or SIGTERM or EXIT ;
  • the signal gets trapped, executing kill 0 , which sends SIGTERM to all processes in the group, including the shell itself;
  • go to 1 :)
  • 这可以通过手动取消注册陷阱来解决。

    trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT
    

    更花哨的方式,允许打印收到的信号,避免 "终止:"信息。

    #!/usr/bin/env bash
    trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
      local func="$1"; shift
      for sig in "$@"; do
        trap "$func $sig" "$sig"
    stop() {
      trap - SIGINT EXIT
      printf '\n%s\n' "received $1, killing child processes"
      kill -s SIGINT 0
    trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP
    { i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
    { i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &
    while true; do read; done
    

    UPD增加了一个最小的例子;改进了stop函数,以避免去掉不必要的信号,并从输出中隐藏 "Terminated: "信息。谢谢Trevor Boyd Smith for the suggestions!

    stop() 中,你提供了第一个参数作为信号编号,但是你硬编码了哪些信号被取消注册。你可以在 stop() 函数中使用第一个参数来取消注册(这样做有可能会停止其他递归信号(除了硬编码的3个))。
    @TrevorBoydSmith,这不会像预期的那样工作,我猜。例如,shell可能会被 SIGINT 杀死,但 kill 0 会发送 SIGTERM ,这将再次被捕获。但这不会产生无限递归,因为 SIGTERM 会在第二次调用 stop 时被解除陷阱。
    也许, trap - $1 && kill -s $1 0 的效果会更好。我将测试并更新这个答案。谢谢你的好主意!:)
    不, trap - $1 && kill -s $1 0 也不起作用,因为我们不能用 EXIT 来杀死。但是,只要把 TERM 去掉就可以了,因为 kill 会默认发送这个信号。
    @Sapphire_Brick 完成了,现在应该更难误解信息了。
    raytraced
    raytraced
    发布于 2019-02-08
    0 人赞同

    trap 'kill $(jobs -p)' EXIT

    我只会对Johannes的答案做一些细微的改动,并使用jobs -pr来限制对运行中的进程进行查杀,并在列表中增加一些信号。

    trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
        
    为什么不把停止的工作也杀掉呢?在Bash中,EXIT陷阱在SIGINT和SIGTERM的情况下也会被运行,所以在这种信号的情况下,陷阱会被调用两次。
    tdaitx
    tdaitx
    发布于 2019-02-08
    0 人赞同

    为了安全起见,我发现最好定义一个清理函数并从陷阱中调用它。

    cleanup() {
            local pids=$(jobs -pr)
            [ -n "$pids" ] && kill $pids
    trap "cleanup" INT QUIT TERM EXIT [...]
    

    或完全避开该功能。

    trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]
    

    为什么?因为通过简单地使用trap 'kill $(jobs -pr)' [...],人们假设有will陷阱条件发出信号时,后台作业正在运行。当没有工作时,会看到以下(或类似)信息。

    kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
    

    因为jobs -pr是空的--我在那个'陷阱'中结束了(双关语)。

    这个测试案例 [ -n "$(jobs -pr)" ] 在我的bash上不起作用。我使用GNU bash,版本4.2.46(2)-release(x86_64-redhat-linux-gnu)。"kill: usage "的信息不断跳出。
    我怀疑这与 jobs -pr 没有返回后台进程的子进程的PID有关。它并没有把整个进程树拆掉,只是把根部修剪掉。
    Delaware
    Delaware
    发布于 2019-02-08
    0 人赞同
    function cleanup_func {
        sleep 0.5
        echo cleanup
    trap "exit \$exit_code" INT TERM
    trap "exit_code=\$?; cleanup_func; kill 0" EXIT
    # exit 1
    # exit 0
    

    Like https://stackoverflow.com/a/22644006/10082476,但加入了退出代码

    INT TERM 的陷阱中, exit_code 来自哪里?
    Orsiris de Jong
    Orsiris de Jong
    发布于 2019-02-08
    0 人赞同

    一个不错的版本,在Linux、BSD和MacOS X下工作。首先尝试发送SIGTERM,如果不成功,在10秒后杀死进程。

    KillJobs() {
        for job in $(jobs -p); do
                kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)
    TrapQuit() {
        # Whatever you need to clean here
        KillJobs
    trap TrapQuit EXIT
    

    请注意,工作不包括孙子的过程。

    nh2
    nh2
    发布于 2019-02-08
    0 人赞同

    我对@tokland的答案进行了改编,并结合了来自@tokland的知识。 http://veithen.github.io/2014/11/16/sigterm-propagation.html 当我注意到 trap 在我运行一个前台进程(不是用 & 做后台)的情况下不会触发。

    #!/bin/bash
    # killable-shell.sh: Kills itself and all children (the whole process group) when killed.
    # Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
    # Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
    trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT
    echo $@
    PID=$!
    wait $PID
    trap - SIGINT SIGTERM EXIT
    wait $PID
    

    它的工作实例。

    $ bash killable-shell.sh sleep 100
    sleep 100
    [1]  + 31568 suspended  bash killable-shell.sh sleep 100
    $ ps aux | grep "sleep"
    niklas   31568  0.0  0.0  19640  1440 pts/18   T    01:30   0:00 bash killable-shell.sh sleep 100
    niklas   31569  0.0  0.0  14404   616 pts/18   T    01:30   0:00 sleep 100
    niklas   31605  0.0  0.0  18956   936 pts/18   S+   01:30   0:00 grep --color=auto sleep
    [1]  + 31568 continued  bash killable-shell.sh sleep 100
    $ kill 31568
    Caught SIGTERM, sending SIGTERM to process group
    [1]  + 31568 terminated  bash killable-shell.sh sleep 100
    $ ps aux | grep "sleep"
    niklas   31717  0.0  0.0  18956   936 pts/18   S+   01:31   0:00 grep --color=auto sleep
        
    Dino Dini
    Dino Dini
    发布于 2019-02-08
    0 人赞同

    我终于找到了一个似乎在所有情况下都有效的解决方案,以递归方式杀死所有的下降,无论它们是作业,还是子进程。这里的其他解决方案似乎都失败了,比如说。

    while ! ffmpeg ....
      sleep 1
    

    在我的情况下,ffmpeg会在父脚本退出后继续运行。

    我找到了一个解决方案here递归地获取所有子进程的PID,并在陷阱处理程序中这样使用。

    cleanup() {
        # kill all processes whose parent is this process
        kill $(pidtree $$ | tac)
    pidtree() (
        [ -n "$ZSH_VERSION"  ] && setopt shwordsplit
        declare -A CHILDS
        while read P PP;do
            CHILDS[$PP]+=" $P"
        done < <(ps -e -o pid= -o ppid=)
        walk() {
            echo $1
            for i in ${CHILDS[$1]};do
                walk $i
        for i in "$@";do
            walk $i
    trap cleanup EXIT
    

    上述内容放在bash脚本的开头,可以成功地杀死所有的子进程。注意,pidtree是用$$调用的,$$是正在退出的bash脚本的PID,PID的列表(每行一个)是用tac反转的,以尝试和确保子进程之后才杀死子进程,以避免循环中可能出现的竞赛条件,比如我给出的例子。

    Oli
    Oli
    发布于 2019-02-08
    0 人赞同

    所以要对脚本的加载进行编程。运行一个 killall (或者你的操作系统上有的)命令,在脚本完成后立即执行。

    orip
    orip
    发布于 2019-02-08
    0 人赞同

    另一个选择是让脚本把自己设定为进程组的领导者,并在退出时在你的进程组上捕获一个 killpg。

    作为过程组长,你如何设定过程?什么是 "killpg"?
    michaeljt
    michaeljt
    发布于 2019-02-08
    0 人赞同

    如果在子壳中调用jobs -p,它在所有的shell中都不工作,可能除非它的输出被重定向到一个文件而不是管道中。 (我认为它最初只用于交互式使用)。

    下面的情况如何。

    trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]
    

    Debian的dash shell需要调用 "jobs",如果当前工作("%%")丢失,它就无法更新。