【发布时间】:2010-09-26 11:43:53
【问题描述】:
我正在寻找一种方法来在我的顶级脚本退出时清理混乱。
特别是如果我想使用set -e,我希望脚本退出时后台进程会死。
【问题讨论】:
标签: shell
我正在寻找一种方法来在我的顶级脚本退出时清理混乱。
特别是如果我想使用set -e,我希望脚本退出时后台进程会死。
【问题讨论】:
标签: shell
@tokland's answer 中描述的trap 'kill 0' SIGINT SIGTERM EXIT 解决方案非常好,但在使用它时是最新的 Bash crashes with a segmentation fault。这是因为从 v. 4.3 开始的 Bash 允许陷阱递归,在这种情况下它变得无限:
SIGINT或SIGTERM或EXIT;kill 0,将SIGTERM发送到组中的所有进程,包括shell本身;这可以通过手动注销陷阱来解决:
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
UPD:添加了一个最小示例;改进了stop 函数,以避免取消捕获不必要的信号并从输出中隐藏“终止:”消息。感谢Trevor Boyd Smith 的建议!
【讨论】:
stop() 中,您提供第一个参数作为信号编号,但随后您硬编码要取消注册的信号。您可以使用第一个参数在 stop() 函数中取消注册,而不是硬编码要注销的信号(这样做可能会停止其他递归信号(除了 3 个硬编码)。
SIGINT 杀死,但kill 0 发送SIGTERM,这将再次陷入困境。不过,这不会产生无限递归,因为 SIGTERM 将在第二次 stop 调用期间被解除陷阱。
trap - $1 && kill -s $1 0 可能会更好。我将测试并更新这个答案。谢谢你的好主意! :)
trap - $1 && kill -s $1 0 也不行,因为我们不能用EXIT 杀人。但是de-trap TERM真的足够了,因为kill默认发送这个信号。
更新:https://stackoverflow.com/a/53714583/302079 通过添加退出状态和清理功能改进了这一点。
trap "exit" INT TERM
trap "kill 0" EXIT
为什么要转换INT 和TERM 来退出?因为两者都应该在不进入无限循环的情况下触发kill 0。
为什么在EXIT 上触发kill 0?因为正常的脚本退出也应该触发kill 0。
为什么是kill 0?因为嵌套的子壳也需要被杀死。这将删除the whole process tree。
【讨论】:
kill 0 的具体含义/作用?
为了多样性,我将发布 https://stackoverflow.com/a/2173421/102484 的变体,因为该解决方案会在我的环境中导致消息“已终止”:
trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM 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,但添加了退出代码
【讨论】:
exit_code 在INT TERM 陷阱中来自哪里?
一个在 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 不会在我正在运行前台进程(不是& 的后台)时触发时,我根据@tokland 的答案结合http://veithen.github.io/2014/11/16/sigterm-propagation.html 的知识进行了改编:
#!/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
【讨论】:
【讨论】:
4.3.30(1)-release 上遇到了这个问题,它也是confirmed on Ubuntu。不过有一个obvoius wokaround :)
-$$。它评估为'--1234。在 kill 手册页 // 内置手册页中,前导破折号指定要发送的信号。但是 - 可能会阻止它,但是否则前面的破折号是未记录的。有什么帮助吗?man 2 kill,它解释了当 PID 为负数时,信号会发送到具有提供的 ID (en.wikipedia.org/wiki/Process_group) 的进程组中的所有进程。令人困惑的是,man 1 kill 或 man bash 中没有提到这一点,并且可能被视为文档中的错误。
为了安全起见,我发现最好定义一个清理函数并从陷阱中调用它:
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)-release (x86_64-redhat-linux-gnu)。 “kill:usage”消息不断弹出。
jobs -pr 不返回后台进程子进程的 PID 有关。它不会拆掉整个流程树,只会修剪根部。
jobs -p 如果在子 shell 中调用,则不能在所有 shell 中工作,可能除非它的输出被重定向到文件而不是管道。 (我认为它最初仅用于交互使用。)
下面的呢:
trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]
Debian 的 dash shell 需要调用“jobs”,如果当前作业(“%%”)丢失,它将无法更新它。
【讨论】:
trap 'echo in trap; set -x; trap - TERM EXIT; while kill %% 2>/dev/null; do jobs > /dev/null; done; set +x' INT TERM EXIT; sleep 100 & while true; do printf .; sleep 1; done 如果你在 Bash (5.0.3) 中运行它并尝试终止,似乎有一个无限循环。但是,如果您再次终止它,它会起作用。即使是 Dash (0.5.10.2-6),您也必须终止它两次。
trap 'kill $(jobs -p)' 退出
我只会对 Johannes 的回答进行微小的更改,并使用 jobs -pr 来限制对正在运行的进程的终止,并在列表中添加更多信号:
trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
【讨论】:
要清理一些混乱,可以使用trap。它可以提供特定信号到达时执行的内容列表:
trap "echo hello" SIGINT
但也可用于在 shell 退出时执行某些操作:
trap "killall background" EXIT
它是内置的,所以help trap 会给你信息(与 bash 一起使用)。如果你只想杀死后台作业,你可以这样做
trap 'kill $(jobs -p)' EXIT
注意使用单个',以防止shell 立即替换$()。
【讨论】:
kill $(jobs -p) 在 dash 中不起作用,因为它在子 shell 中执行命令替换(请参阅 man dash 中的命令替换)
killall background 应该是占位符吗? background 不在手册页中...
kill $(jobs -p) 很好,但在没有后台作业时打印“kill”的使用信息。恕我直言,bash 的最佳方式是jobs -p | xargs -r kill
另一种选择是让脚本将自己设置为进程组领导,并在退出时在进程组上捕获 killpg。
【讨论】:
所以脚本加载脚本。运行 killall(或您的操作系统上可用的任何内容)命令,该命令会在脚本完成后立即执行。
【讨论】: