【问题标题】:How do I terminate all the subshell processes?如何终止所有 subshel​​l 进程?
【发布时间】:2011-12-02 22:30:54
【问题描述】:

我有一个 bash 脚本来测试服务器在负载下的性能。

num=1
if [ $# -gt 0 ]; then
    num=$1
fi
for i in {1 .. $num}; do
    (while true; do
        { time curl --silent 'http://localhost'; } 2>&1 | grep real
    done) &
done        

wait

当我按下 Ctrl-C 时,主进程退出,但后台循环继续运行。我如何让他们全部退出?或者有没有更好的方法来生成可配置数量的并行执行的逻辑循环?

【问题讨论】:

    标签: bash cygwin


    【解决方案1】:

    这是一个更简单的解决方案 - 只需在脚本顶部添加以下行:

    trap "kill 0" SIGINT
    

    杀死0 将信号发送到当前进程组中的所有进程。

    【讨论】:

    • 听起来不错,很干净,但我不明白如何管理进程组。是否保证我正在生成的所有后台进程,并且没有其他进程与脚本在同一进程组中?
    • 是的,这是进程组的默认行为。除非您编写的代码明确地进行系统调用来更改进程组,否则您会没事的。
    • @RussellDavis 这很干净,效果很好。我必须在从主脚本生成的所有 shell 脚本中添加陷阱才能使其正常工作。
    • 有什么特别的理由不像这个答案那样捕获 SIGTERM 和 EXIT 吗? stackoverflow.com/a/2173421/179583
    • 我很好奇你是怎么发现这个的。我在 kill 的手册页中找不到它。
    【解决方案2】:

    答案有点晚,但对我来说,kill 0kill $(jobs -p) 之类的解决方案太过分了(杀死所有子进程)。

    如果您只是想确保整理一个特定的子进程(及其自己的子进程),那么更好的解决方案是使用子进程的 PID 按进程组 (PGID) 杀死,如下所示:

    set -m
    ./some_child_script.sh &
    some_pid=$!
    
    kill -- -${some_pid}
    

    首先,set -m 命令将启用作业管理(如果尚未启用),这很重要,否则所有命令、子 shell 等将被分配到与您的父脚本相同的进程组(与您在终端中手动运行命令不同),并且 kill 只会给出“没有这样的进程”错误。这需要在您运行您希望作为一个组进行管理的后台命令之前调用(或者如果您有多个,则只需在脚本启动时调用它)。

    其次,注意kill 的参数是否定的,这表明你要杀死整个进程组。默认情况下,进程组 ID 与组中的第一个命令相同,因此我们只需在使用 $! 获取的 PID 前添加一个减号即可获得它。如果您需要在更复杂的情况下获取进程组 ID,则需要使用 ps -o pgid= ${some_pid},然后在其中添加减号。

    最后,注意选项 -- 的显式结尾的使用,这很重要,否则进程组参数将被视为选项(信号号),kill 会抱怨它没有足够的论据。只有当进程组参数是您希望终止的第一个参数时,您才需要此参数。

    这是一个后台超时过程的简化示例,以及如何尽可能多地清理:

    #!/bin/bash
    # Use the overkill method in case we're terminated ourselves
    trap 'kill $(jobs -p | xargs)' SIGINT SIGHUP SIGTERM EXIT
    
    # Setup a simple timeout command (an echo)
    set -m
    { sleep 3600; echo "Operation took longer than an hour"; } &
    timeout_pid=$!
    
    # Run our actual operation here
    do_something
    
    # Cancel our timeout
    kill -- -${timeout_pid} >/dev/null 2>&1
    wait -- -${timeout_pid} >/dev/null 2>&1
    printf '' 2>&1
    

    这应该在所有合理的情况下干净地处理取消这个简单的超时;唯一无法处理的情况是脚本被立即终止 (kill -9),因为它没有机会清理。

    我还添加了一个wait,后跟一个无操作(printf ''),这是为了抑制可能由kill 命令引起的“终止”消息,这有点像hack ,但根据我的经验是足够可靠的。

    【讨论】:

    • wait + printf 方法对我不起作用(使用 bourne shell),但在 kill 之后添加 set +m 会抑制“终止”消息。
    【解决方案3】:

    一种杀死子shell的方法,但不是自我:

    kill $(jobs -p)
    

    【讨论】:

      【解决方案4】:

      您需要使用job control,不幸的是,这有点复杂。如果这些是您希望运行的唯一后台作业,您可以运行如下命令:

      jobs \
        | perl -ne 'print "$1\n" if m/^\[(\d+)\][+-]? +Running/;' \
        | while read -r ; do kill %"$REPLY" ; done
      

      jobs 以如下格式打印所有活动作业(正在运行的作业,加上最近完成或终止的作业)的列表:

      [1]   Running                 sleep 10 &
      [2]   Running                 sleep 10 &
      [3]   Running                 sleep 10 &
      [4]   Running                 sleep 10 &
      [5]   Running                 sleep 10 &
      [6]   Running                 sleep 10 &
      [7]   Running                 sleep 10 &
      [8]   Running                 sleep 10 &
      [9]-  Running                 sleep 10 &
      [10]+  Running                 sleep 10 &
      

      (这些是我通过运行for i in {1..10} ; do sleep 10 & done 启动的作业。)

      perl -ne ... 是我使用 Perl 提取正在运行的作业的作业号;如果您愿意,显然可以使用不同的工具。如果您的jobs 具有不同的输出格式,您可能需要修改此脚本;但是上面的输出也在 Cygwin 上,所以很可能和你的一样。

      read -r 从标准输入中读取“原始”行,并将其保存到变量$REPLY 中。 kill %"$REPLY" 类似于 kill %1,它会“杀死”(发送中断信号)第 1 号作业。(不要与 kill 1 混淆,后者会杀死第 1 号进程。 ) while read -r ; do kill %"$REPLY" ; done 一起检查 Perl 脚本打印的每个作业编号,并杀死它。

      顺便说一句,你的for i in {1 .. $num} 不会做你所期望的,因为大括号扩展是在之前 参数扩展处理的,所以你所拥有的相当于for i in "{1" .. "$num}"。 (无论如何,大括号扩展内不能有空格。)不幸的是,我不知道一个干净的替代方案。我认为您必须执行for i in $(bash -c "{1..$num}") 之类的操作,或者切换到算术for-loop 之类的。

      顺便说一句,您不需要将 while 循环括在括号中; & 已经导致作业在子shell 中运行。

      【讨论】:

      • 感谢您的提示,尤其感谢您提供的 btw 提示。我不是 bash 专家,所以我是通过谷歌搜索来写的。
      • 不客气!我确切地知道你的意思。我也不是 Bash 专家,直到大约一年前,当我找到 Bash 参考手册(在我的答案中链接到)时,我和你在同一条船上。它完全改变了我的生活,或者至少是 Bash 的一部分。 :-P
      【解决方案5】:

      这是我的最终解决方案。我正在使用数组变量跟踪子 shell 进程 ID,并捕获 Ctrl-C 信号以杀死它们。

      declare -a subs #array of subshell pids
      
      function kill_subs() {
          for pid in ${subs[@]}; do
              kill $pid
          done
          exit 0 
      }
      
      num=1 if [ $# -gt 0 ]; then
          num=$1 fi
      
      for ((i=0;i < $num; i++)); do
          while true; do
             { time curl --silent 'http://localhost'; } 2>&1 | grep real
          done &
      
          subs[$i]=$! #grab the pid of the subshell 
      done
      
      trap kill_subs 1 2 15
      
      wait
      

      【讨论】:

      • 我切换到接受的解决方案。把它留在这里,以防有人因为其他原因发现它有用。
      猜你喜欢
      • 2011-01-21
      • 2014-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-22
      • 1970-01-01
      相关资源
      最近更新 更多