【问题标题】:How do I kill background processes / jobs when my shell script exits?当我的 shell 脚本退出时,如何终止后台进程/作业?
【发布时间】:2010-09-26 11:43:53
【问题描述】:

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

特别是如果我想使用set -e,我希望脚本退出时后台进程会死。

【问题讨论】:

    标签: shell


    【解决方案1】:

    @tokland's answer 中描述的trap 'kill 0' SIGINT SIGTERM EXIT 解决方案非常好,但在使用它时是最新的 Bash crashes with a segmentation fault。这是因为从 v. 4.3 开始的 Bash 允许陷阱递归,在这种情况下它变得无限:

    1. shell进程接收SIGINTSIGTERMEXIT;
    2. 信号被捕获,执行kill 0,将SIGTERM发送到组中的所有进程,包括shell本身;
    3. 转到 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"
      done
    }
    
    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 函数,以避免取消捕获不必要的信号并从输出中隐藏“终止:”消息。感谢Trevor Boyd Smith 的建议!

    【讨论】:

    • stop() 中,您提供第一个参数作为信号编号,但随后您硬编码要取消注册的信号。您可以使用第一个参数在 stop() 函数中取消注册,而不是硬编码要注销的信号(这样做可能会停止其他递归信号(除了 3 个硬编码)。
    • @TrevorBoydSmith,我猜这不会按预期工作。例如,shell 可能被SIGINT 杀死,但kill 0 发送SIGTERM,这将再次陷入困境。不过,这不会产生无限递归,因为 SIGTERM 将在第二次 stop 调用期间被解除陷阱。
    • trap - $1 && kill -s $1 0 可能会更好。我将测试并更新这个答案。谢谢你的好主意! :)
    • 不,trap - $1 && kill -s $1 0 也不行,因为我们不能用EXIT 杀人。但是de-trap TERM真的足够了,因为kill默认发送这个信号。
    • @Sapphire_Brick 完成,现在应该更难误解消息了。
    【解决方案2】:

    更新: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 的具体含义/作用?
    • 这很棒,但也杀死了我的父 shell :-(
    • 这个解决方案简直是矫枉过正。 kill 0(在我的脚本中)毁了我的整个 X 会话!也许在某些情况下 kill 0 可能很有用,但这并不能改变它不是通用解决方案的事实,除非有充分的理由使用它,否则应尽可能避免使用它。最好添加一个警告,它可能会杀死父 shell 甚至整个 X 会话,而不仅仅是脚本的后台作业!
    • 虽然在某些情况下这可能是一个有趣的解决方案,正如@vidstige 所指出的那样,这将杀死包含启动进程的整个进程组(即父shell大多数情况下)。当您通过 IDE 运行脚本时,绝对不是您想要的。
    【解决方案3】:

    为了多样性,我将发布 https://stackoverflow.com/a/2173421/102484 的变体,因为该解决方案会在我的环境中导致消息“已终止”:

    trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
    

    【讨论】:

      【解决方案4】:
      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
      

      类似于https://stackoverflow.com/a/22644006/10082476,但添加了退出代码

      【讨论】:

      • exit_codeINT TERM 陷阱中来自哪里?
      【解决方案5】:

      一个在 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 &)
      
          done
      }
      
      TrapQuit() {
          # Whatever you need to clean here
          KillJobs
      }
      
      trap TrapQuit EXIT
      

      请注意,作业不包括孙进程。

      【讨论】:

        【解决方案6】:

        当我注意到trap 不会在我正在运行前台进程(不是& 的后台)时触发时,我根据@tokland 的答案结合http://veithen.github.io/2014/11/16/sigterm-propagation.html 的知识进行了改编:

        #!/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
        ^Z
        [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
        
        $ bg
        [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
        

        【讨论】:

          【解决方案7】:

          这对我有用(感谢评论者的改进):

          trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
          
          • kill -- -$$ 向整个进程组发送SIGTERM,从而也杀死后代。

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

          【讨论】:

          • 整体上应该可以正常工作,但是子进程可能会改变进程组。另一方面,它不需要作业控制,也可能会导致其他解决方案遗漏一些孙进程。
          • 注意,“kill 0”也会杀死一个父 bash 脚本。您可能想使用“kill -- -$BASHPID”来仅杀死当前脚本的子级。如果你的 bash 版本中没有 $BASHPID,你可以 export BASHPID=$(sh -c 'echo $PPID')
          • 感谢您提供清晰明了的解决方案!不幸的是,它会导致 Bash 4.3 出现段错误,从而允许陷阱递归。我在 OSX 上的4.3.30(1)-release 上遇到了这个问题,它也是confirmed on Ubuntu。不过有一个obvoius wokaround :)
          • 我不太明白-$$。它评估为'-`,例如-1234。在 kill 手册页 // 内置手册页中,前导破折号指定要发送的信号。但是 - 可能会阻止它,但是否则前面的破折号是未记录的。有什么帮助吗?
          • @EvanBenn:检查man 2 kill,它解释了当 PID 为负数时,信号会发送到具有提供的 ID (en.wikipedia.org/wiki/Process_group) 的进程组中的所有进程。令人困惑的是,man 1 killman bash 中没有提到这一点,并且可能被视为文档中的错误。
          【解决方案8】:

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

          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)' [...] 可以假设当陷阱条件发出信号时会有后台作业在运行。当没有作业时,您将看到以下(或类似)消息:

          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 有关。它不会拆掉整个流程树,只会修剪根部。
          【解决方案9】:

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

          下面的呢:

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

          Debian 的 dash shell 需要调用“jobs”,如果当前作业(“%%”)丢失,它将无法更新它。

          【讨论】:

          • 嗯很有趣的方法,但它似乎不起作用。考虑 scipt trap 'echo in trap; set -x; trap - TERM EXIT; while kill %% 2>/dev/null; do jobs > /dev/null; done; set +x' INT TERM EXIT; sleep 100 & while true; do printf .; sleep 1; done 如果你在 Bash (5.0.3) 中运行它并尝试终止,似乎有一个无限循环。但是,如果您再次终止它,它会起作用。即使是 Dash (0.5.10.2-6),您也必须终止它两次。
          【解决方案10】:

          trap 'kill $(jobs -p)' 退出

          我只会对 Johannes 的回答进行微小的更改,并使用 jobs -pr 来限制对正在运行的进程的终止,并在列表中添加更多信号:

          trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
          

          【讨论】:

          • 为什么不也杀死停止的作业呢?在 Bash 中 EXIT 陷阱也会在 SIGINT 和 SIGTERM 的情况下运行,因此在这种信号的情况下会调用两次陷阱。
          【解决方案11】:

          要清理一些混乱,可以使用trap。它可以提供特定信号到达时执行的内容列表:

          trap "echo hello" SIGINT
          

          但也可用于在 shell 退出时执行某些操作:

          trap "killall background" EXIT
          

          它是内置的,所以help trap 会给你信息(与 bash 一起使用)。如果你只想杀死后台作业,你可以这样做

          trap 'kill $(jobs -p)' EXIT
          

          注意使用单个',以防止shell 立即替换$()

          【讨论】:

          • 那你如何只杀死所有child? (或者我错过了什么明显的东西)
          • killall 会杀死你的孩子,但不会杀死你
          • kill $(jobs -p) 在 dash 中不起作用,因为它在子 shell 中执行命令替换(请参阅 man dash 中的命令替换)
          • killall background 应该是占位符吗? background 不在手册页中...
          • kill $(jobs -p) 很好,但在没有后台作业时打印“kill”的使用信息。恕我直言,bash 的最佳方式是jobs -p | xargs -r kill
          【解决方案12】:

          另一种选择是让脚本将自己设置为进程组领导,并在退出时在进程组上捕获 killpg。

          【讨论】:

          • 如何设置流程为流程组长?什么是“killpg”?
          【解决方案13】:

          所以脚本加载脚本。运行 killall(或您的操作系统上可用的任何内容)命令,该命令会在脚本完成后立即执行。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-12-10
            • 1970-01-01
            • 2015-03-14
            • 2015-10-31
            • 1970-01-01
            • 2016-08-21
            • 2016-09-29
            • 1970-01-01
            相关资源
            最近更新 更多