当主进程退出时,如何在bash中杀死所有后台进程

12

我有一个bash脚本,可以作为后台进程运行无限个命令:

#!/bin/bash

function xyz() {
  # some awk command
}

endlesscommand "param 1" | xyz &  # async
pids=$!
endlesscommand "param 2" | xyz &  # async
pids="$pids "$!
endlesscommand "param 3" | xyz    # sync so the script doesn't leave

唯一停止此脚本的方法是(必须是)Ctrl-C或kill,当发生这种情况时,我需要杀死$pids变量中列出的所有后台进程。

我该怎么做?

如果可能在主进程上捕获kill信号并在发生时执行函数(关闭钩子),我会这样做:

for $pid in $pids; do kill $pid; done;

但我找不到如何做到这一点...


你需要使用陷阱... - devnull
没有必要维护一个明确的子进程列表;你可以调用 jobs -p 来获取仍在运行的子进程的进程ID。 - chepner
3个回答

14
这是一种无需跟踪pids的陷阱:
trap 'jobs -p | xargs kill' EXIT

编辑: @Barmar问这个方法是否适用于非源代码脚本,在那里作业控制通常不可用。它是可行的。考虑以下脚本:

$ cat no-job-control
#! /bin/bash

set -e -o pipefail

# Prove job control is off
if suspend
then
  echo suspended
else
  echo suspension failed, job control must be off
fi

echo

# Set up the trap
trap 'jobs -p | xargs kill' EXIT

# Make some work
(echo '=> Starting 0'; sleep 5; echo '=> Finishing 0') &
(echo '=> Starting 1'; sleep 5; echo '=> Finishing 1') &
(echo '=> Starting 2'; sleep 5; echo '=> Finishing 2') &

echo "What's in jobs -p?"
echo

jobs -p

echo
echo "Ok, exiting now"
echo

当我们运行时,会看到三个组长的pid,然后看到它们被杀死:
$ ./no-job-control
./no-job-control: line 6: suspend: cannot suspend: no job control
suspension failed, job control must be off

=> Starting 0
What's in jobs -p?
=> Starting 1

54098
54099
54100

Ok, exiting now

=> Starting 2
./no-job-control: line 31: 54098 Terminated: 15          ( echo '=> Starting 0'; sleep 5; echo '=> Finishing 0' )
./no-job-control: line 31: 54099 Terminated: 15          ( echo '=> Starting 1'; sleep 5; echo '=> Finishing 1' )
./no-job-control: line 31: 54100 Terminated: 15          ( echo '=> Starting 2'; sleep 5; echo '=> Finishing 2' )

如果我们将trap这一行注释掉并重新运行,三个作业就不会死亡,事实上它们会在几秒钟后打印出它们的最终消息。请注意,返回的提示与最终输出交错。
$ ./no-job-control
./no-job-control: line 6: suspend: cannot suspend: no job control
suspension failed, job control must be off

=> Starting 0
What's in jobs -p?

54110
54111
54112
=> Starting 1

Ok, exiting now

=> Starting 2
$ => Finishing 0
=> Finishing 2
=> Finishing 1

1
谢谢,非常好用,甚至可以使用管道! :) 而且它甚至可以更短: trap 'jobs -p | xargs kill' EXIT - fabien
即使脚本没有启用作业控制,jobs -p 在脚本内部是否真的有效? - Barmar
是的,我在发表评论后进行了一个简单的测试。令人惊讶。 - Barmar

1
您可以使用pgrep和一个函数来杀死所有在主进程下创建的进程,就像这样。这不仅会杀死直接子进程,还会杀死它们下面创建的进程。
#!/bin/bash

function killchildren {
    local LIST=() IFS=$'\n' A
    read -a LIST -d '' < <(exec pgrep -P "$1")
    local A SIGNAL="${2:-SIGTERM}"
    for A in "${LIST[@]}"; do
        killchildren_ "$A" "$SIGNAL"
    done
}

function killchildren_ {
    local LIST=()
    read -a LIST -d '' < <(exec pgrep -P "$1")
    kill -s "$2" "$1"
    if [[ ${#LIST[@]} -gt 0 ]]; then
        local A
        for A in "${LIST[@]}"; do
            killchildren_ "$A" "$2"
        done
    fi
}

trap 'killchildren "$BASHPID"' EXIT

endlesscommand "param 1" &
endlesscommand "param 2" &
endlesscommand "param 3" &

while pgrep -P "$BASHPID" >/dev/null; do
    wait
done

关于你的原始代码,最好只使用数组,并且不需要使用for循环:
#!/bin/bash

trap 'kill "${pids[@]}"' EXIT

pids=()
endlesscommand "param 1" &  # async
pids+=("$!")
endlesscommand "param 2" &  # async
pids+=("$!")
endlesscommand "param 3" &  # syncing this is not a good idea since if the main process would end along with it if it ends earlier.
pids+=("$!")

while pgrep -P "$BASHPID" >/dev/null; do
    wait
done

原始函数参考:http://www.linuxquestions.org/questions/blog/konsolebox-210384/bash-functions-to-list-and-kill-or-send-signals-to-process-trees-34624/


我尝试了最后一个解决方案,它有效...但是如果我的无限命令与其他程序/函数一起使用管道,则无效(我更新了我的问题)。 最终,我使用了您的pgrep循环和phs解决方案,它非常好用。 :) - fabien

0
kill `ps axl | grep "endlesscommand" | awk '{printf $4" "}'`

这将查找影响“endlesscommand”的父进程


“kill -9” 不应轻易使用。简单的“kill”就足够了。此外,这会杀死所有正在运行“endlesscommand”的进程,而不仅仅是由被杀死的父进程启动的进程。 - chepner
@chepner 对于衍生进程来说不正确... OP 想要杀死所有涉及的进程,所以我不明白为什么 kill -9 是不安全的? - iamauser
"kill -9" 不会给被杀进程任何机会来清理自己:关闭文件,刷新缓冲区等。 - chepner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接