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


193

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

尤其是如果我想使用set -e,希望脚本退出时后台进程会死掉。

Answers:


186

可以清理一些乱七八糟的东西trap。它可以提供在特定信号到达时执行的工作清单:

trap "echo hello" SIGINT

但如果shell退出,也可以用来执行某些操作:

trap "killall background" EXIT

它是内置的,因此help trap将为您提供信息(适用于bash)。如果您只想杀死后台工作,则可以

trap 'kill $(jobs -p)' EXIT

当心使用single ',以防止shell $()立即替换。


那你怎么杀所有的孩子呢?(或者我是否想念明显的东西)
elmarco

18
Killall杀死您的孩子,但不杀死您
Orip

3
kill $(jobs -p)在破折号中不起作用,因为它在子shell中执行命令替换(请参见man破折号中的命令替换)
user1431317

7
killall background认为是一个占位符?background不在手册页中……
Evan Benn

170

这对我有用(感谢评论者的改进):

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

  • EXIT使用时指定信号很有用set -e此处有更多详细信息)。


1
总体上应该可以正常工作,但是子进程可能会更改进程组。另一方面,它不需要作业控制,并且可能还会使其他解决方案错过一些孙子流程。
michaeljt 2012年

5
注意,“ kill 0”也会杀死父bash脚本。您可能要使用“ kill--$ BASHPID”仅杀死当前脚本的子代。如果您的bash版本中没有$ BASHPID,则可以导出BASHPID = $(sh -c'echo $ PPID')
ACyclic 2013年

2
谢谢您提供清晰明了的解决方案!不幸的是,它会隔离Bash 4.3,从而允许陷阱递归。我4.3.30(1)-release在OSX 上遇到了这个问题,在Ubuntu上得到了证实。不过,这里有一个让人讨厌的地方:)
skozin 2015年

4
我不太明白-$$。评估为'-<PID>`例如-1234。在kill联机帮助页//内置的联机帮助页中,前导破折号指定要发送的信号。但是-可能阻止了该操作,但是如果没有,则前导破折号没有记载。有什么帮助吗?
埃文·本

4
@EvanBenn:Check man 2 kill,它说明当PID为负数时,信号将以提供的ID(en.wikipedia.org/wiki/Process_group)发送到流程组中的所有流程。令人困惑的是,man 1 kill或中没有提及此问题man bash,并且在文档中可能将其视为错误。
user001

111

更新:https : //stackoverflow.com/a/53714583/302079通过添加退出状态和清除功能来改善此问题。

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

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

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

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


3
对于我在Debian上的案例,唯一的解决方案。
MindlessRanger 2015年

3
Johannes Schaub的回答或tokland提供的回答都没有杀死我的shell脚本启动的后台进程(在Debian上)。此解决方案有效。我不知道为什么这个答案没有得到更多支持。您能否进一步说明确切kill 0含义/含义?
josch

7
这很棒,但也杀死了我的父外壳:-(
vidstige

5
从字面上看,这种解决方案是过大的。杀死0(在我的脚本内)破坏了我的整个X会话!也许在某些情况下,kill 0是有用的,但这并不能改变它不是通用解决方案的事实,除非有充分的理由使用它,否则应避免使用它。最好添加一条警告,它可能会杀死父shell甚至整个X会话,而不仅仅是脚本的后台作业!
Lissanro Rayen

3
尽管在某些情况下这可能是一个有趣的解决方案,但@vidstige指出,这将杀死整个进程组,其中包括启动进程(在大多数情况下为父shell)。通过IDE运行脚本时,绝对不是您想要的东西。
matpen '18

22

陷阱'kill $ {jobs -p)'退出

我只对Johannes的答案做些微改动,并使用jobs -pr来限制正在运行的进程的终止,并在列表中添加一些其他信号:

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

14

@tokland的答案中trap 'kill 0' SIGINT SIGTERM EXIT描述的解决方案非常好,但是最新的Bash 在使用时会出现段错误。这是因为Bash从4.3版开始,允许陷阱递归,在这种情况下,该递归变得无限:

  1. 壳过程接收SIGINTSIGTERM或或EXIT;
  2. 信号被捕获,执行kill 0,并发SIGTERM送到组中的所有进程,包括外壳本身;
  3. 转到1 :)

可以通过手动注销陷阱来解决此问题:

trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT

更花哨的方式,它允许打印收到的信号并避免出现“ Terminated:”消息:

#!/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' "recieved $1, killing children"
  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

UPD:加入最小例子; 改进了的stop功能,可以避免捕获不必要的信号并从输出中隐藏“ Terminated:”消息。感谢Trevor Boyd Smith的建议!


stop()您提供的第一个参数的信号数量,但随后你硬编码信号被注销的。您可以使用第一个参数在stop()函数中注销,而不是对要注销的信号进行硬编码(这样做可能会停止其他递归信号(3个硬编码的信号除外))。
Trevor Boyd Smith

@TrevorBoydSmith,我想这不会按预期工作。例如,外壳可能被杀死了SIGINT,但是kill 0发送了SIGTERM,它将再次被捕获。但是,这不会产生无限递归,因为SIGTERM它将在第二个stop调用期间被释放。
skozin

大概trap - $1 && kill -s $1 0应该会更好。我将测试并更新此答案。谢谢你的好主意!:)
skozin 2015年

不,也trap - $1 && kill -s $1 0不会工作,因为我们不能用杀死EXIT。但是做de-trap确实足够了TERM,因为kill默认情况下发送此信号。
skozin 2015年

我用测试了递归EXITtrap信号处理程序始终只执行一次。
Trevor Boyd Smith

9

为了安全起见,我发现最好定义一个清理函数并从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)' [...]一个就假定在发出陷阱条件时有后台作业在运行。当没有作业时,将看到以下(或类似的)消息:

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

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


这个测试用例[ -n "$(jobs -pr)" ]不适用于我的bash。我使用GNU bash版本4.2.46(2)-发行版(x86_64-redhat-linux-gnu)。“ kill:用法”消息不断弹出。
杜威范德莱斯特

我怀疑这与jobs -pr不返回后台进程子进程的PID 的事实有关。它不会破坏整个过程树,只会修剪根。
杜威范德莱斯特

2

一个很好的版本,可以在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

请注意,工作不包括孙子程序。



1

另一个选择是将脚本本身设置为进程组负责人,并在退出时在进程组上捕获killpg。


0

因此,脚本加载脚本。运行killall脚本完成后立即执行的(或OS上可用的任何命令)命令。


0

如果在子外壳程序中调用Jobs -p,则不能在所有外壳程序中正常工作,除非将其输出重定向到文件而不是管道中,否则可能无法工作。(我认为它最初仅打算用于交互式使用。)

关于以下内容:

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

Debian的破折号外壳需要调用“作业”,如果缺少该作业,它将无法更新当前作业(“ %%”)。


0

当我注意到如果我正在运行前台流程时不会触发该操作时,我对@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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.