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

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


当前回答

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

其他回答

通用解决方案,也在sh中工作(工作不输出任何东西到stdout):

trap "pkill -P $$" EXIT INT

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

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

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

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

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

为了安全起见,我发现最好定义一个清理函数并从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 [...]

为什么?因为通过简单地使用陷阱'kill $(jobs -pr)'[…]当陷阱条件发出信号时,就会有后台作业在运行。当没有工作时,会看到以下(或类似的)消息:

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

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

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

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反转,以尝试确保父进程只在子进程之后被杀死,以避免在循环中可能出现的竞争条件,例如我给出的例子。