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

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

特别是如果我想使用set -e,我希望后台进程在脚本退出时也同时终止。

140210 次浏览

脚本的加载。运行killall(或操作系统中可用的任何命令)命令,脚本完成后立即执行。

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

trap "echo hello" SIGINT

但如果shell退出,也可以用来执行一些东西:

trap "killall background" EXIT

它是内置的,所以help trap会给你信息(适用于bash)。如果你只想消灭后台工作,你可以做到

trap 'kill $(jobs -p)' EXIT

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

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

这对我来说是可行的(多亏了评论的改进):

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

  • 在使用set -e时,指定信号EXIT是有用的(更多细节here)。

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

我只会对Johannes的答案做一些小改动,并使用jobs -pr将kill限制为正在运行的进程,并在列表中添加更多的信号:

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

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

下面这些怎么样:

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

Debian的dash shell需要调用“jobs”,如果缺少当前作业(“%%”),则该shell无法更新。

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

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

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

因为jobs -pr是空的-我以那个“陷阱”结束(双关语)。

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

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

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

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

为什么kill 0 ?因为嵌套的子shell也需要被杀死。这将删除整个过程树

@tokland的回答中描述的trap 'kill 0' SIGINT SIGTERM EXIT解决方案确实很好,但使用它时最新的Bash 分割错误导致崩溃。这是因为Bash,从v. 4.3开始,允许陷阱递归,在这种情况下,它变得无限:

  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

乌利希期刊指南:添加了一个最小示例;改进stop函数,以避免消除不必要的信号和隐藏"终止:"来自输出的消息。感谢特雷弗·博伊德·史密斯的建议!

我把@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
^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

一个可以在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

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

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,但是增加了退出代码

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

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

我终于找到了一个解决方案,在所有情况下都可以递归地终止所有下降,不管它们是作业还是子流程。这里的其他解决方案似乎都失败了,比如:

while ! ffmpeg ....
do
sleep 1
done

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

我找到了一个解决方案在这里,递归地获得所有子进程的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
done
}


for i in "$@";do
walk $i
done
)


trap cleanup EXIT

上面放在bash脚本开头的代码成功地杀死了所有子进程。请注意,pidtree是用$$调用的,$$是正在退出的bash脚本的PID,并且PID列表(每行一个)使用tac反转,以尝试确保父进程只在子进程之后被杀死,以避免在循环中可能出现的竞争条件,例如我给出的例子。