如何终止所有子shell进程?

时间:2011-12-02 22:30:54

标签: bash cygwin

我有一个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时,主进程退出,但后台循环继续运行。如何让它们全部退出?或者是否有更好的方法来产生并行执行的可配置数量的逻辑循环?

5 个答案:

答案 0 :(得分:41)

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

trap "kill 0" SIGINT

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

答案 1 :(得分:3)

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

kill $(jobs -p)

答案 2 :(得分:2)

你需要使用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混淆,这将导致进程< / em> number 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 - 循环或诸如此类的东西。

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

答案 3 :(得分:1)

答案有点晚,但对我来说,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命令引起的“终止”消息,这有点骇人听闻,但以我的经验足够可靠。

答案 4 :(得分:0)

这是我的最终解决方案。我正在使用数组变量跟踪子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