在 USR1 信号后可靠地杀死睡眠进程
Posted
技术标签:
【中文标题】在 USR1 信号后可靠地杀死睡眠进程【英文标题】:Reliably kill sleep process after USR1 signal 【发布时间】:2020-07-26 19:55:12 【问题描述】:我正在编写一个 shell 脚本,它定期执行任务并从另一个进程接收 USR1 信号。
脚本的结构类似于this answer:
#!/bin/bash
trap 'echo "doing some work"' SIGUSR1
while :
do
sleep 10 && echo "doing some work" &
wait $!
done
但是,此脚本的问题是睡眠进程在后台继续,并且仅在超时时终止。 (请注意,当在等待 $! 期间收到 USR1 时,睡眠进程会在其常规超时期间徘徊,但周期性回显确实会被取消。)例如,您可以使用 pkill -0 -c sleep
查看机器上的睡眠进程数。
我读到this page,这表明在陷阱动作中杀死挥之不去的睡眠,例如
#!/bin/bash
pid=
trap '[[ $pid ]] && kill $pid; echo "doing some work"' SIGUSR1
while :
do
sleep 10 && echo "doing some work" &
pid=$!
wait $pid
pid=
done
但是,如果我们快速向 USR1 信号发送垃圾邮件,例如与:
pkill -USR1 trap-test.sh; pkill -USR1 trap-test.sh
然后它会尝试杀死一个已经被杀死的 PID 并打印一个错误。更何况,我不喜欢这段代码。
有没有更好的方法可以在被中断时可靠地杀死分叉的进程?或者是实现相同功能的替代结构?
【问题讨论】:
【参考方案1】:由于后台作业是前台作业的一个分支,它们共享相同的名称 (trap-test.sh
);所以pkill
匹配并发出信号。这以不确定的顺序杀死了后台进程(让sleep
存活,下面将解释)并触发前台进程中的陷阱,从而引发竞争条件。
此外,在您链接的示例中,后台作业始终只是sleep x
,但在您的脚本中是sleep 10 && echo 'doing some work'
;这需要分叉的子shell 等待sleep
终止并有条件地执行echo
。比较这两个:
$ sleep 10 &
[1] 9401
$ pstree 9401
sleep
$
$ sleep 10 && echo foo &
[2] 9410
$ pstree 9410
bash───sleep
让我们从头开始,在终端中重现主要问题。
$ set +m
$ sleep 100 && echo 'doing some work' &
[1] 9923
$ pstree -pg $$
bash(9871,9871)─┬─bash(9923,9871)───sleep(9924,9871)
└─pstree(9927,9871)
$ kill $!
$ pgrep sleep
9924
$ pkill -e sleep
sleep killed (pid 9924)
我禁用了作业控制以部分模拟非交互式 shell 的行为。
杀死后台作业并没有杀死sleep
,我需要手动终止它。发生这种情况是因为发送给进程的信号不会自动广播给其目标的子进程;即sleep
根本没有收到 TERM 信号。
要杀死sleep
以及子shell,我需要将后台作业放入单独的进程组 - 这需要启用作业控制,否则所有作业都放入主如上面pstree
的输出所示,shell 的进程组,并向其发送 TERM 信号,如下所示。
$ set -m
$ sleep 100 && echo 'doing some work' &
[1] 10058
$ pstree -pg $$
bash(9871,9871)─┬─bash(10058,
10058)───sleep(10059,
10058)
└─pstree(10067,10067)
$ kill --
-$!
$
[1]+ Terminated sleep 100 && echo 'doing some work'
$ pgrep sleep
$
通过对这个概念进行一些改进和调整,您的脚本如下所示:
#!/bin/bash -
set -m
usr1_handler()
kill -- -$!
echo 'doing some work'
do_something()
trap '' USR1
sleep 10 && echo 'doing some work'
trap usr1_handler USR1 EXIT
echo "my PID is $$"
while true; do
do_something &
wait
done
这将打印my PID is xxx
(其中xxx
是前台进程的PID)并开始循环。向xxx
(即kill -USR1 xxx
)发送USR1 信号将触发陷阱并导致后台进程及其子进程终止。因此wait
将返回并且循环将继续。
如果您改用pkill
,它仍然可以工作,因为后台进程会忽略 USR1。
有关详细信息,请参阅:
Bash Reference Manual § Special Parameters ($$
and $!
),
POSIX kill
specification (-$!
usage),
POSIX Definitions § Job Control (how job control is implemented in POSIX shells),
Bash Reference Manual § Job Control Basics (how job control works in bash),
POSIX Shell Command Language § Signals And Error Handling,
POSIX wait
specification。
【讨论】:
【参考方案2】:您可能希望使用一个函数来杀死包括子进程在内的整个进程树,尝试很好地杀死它,如果niceness 不起作用,则强制杀死它。 这是您可以添加到脚本中的部分。
在收到 SIGUSR1 或其他退出信号(包括 CTRL+C)时调用 TrapQuit。 您可以在 TrapQuit 中添加任何需要的处理,或者在普通脚本退出时使用退出代码调用它。
# Kill process and children bash 3.2+ implementation
# BusyBox compatible version
function IsInteger
local value="$1"
#if [[ $value =~ ^[0-9]+$ ]]; then
expr "$value" : "^[0-9]\+$" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo 1
else
echo 0
fi
# Portable child (and grandchild) kill function tested under Linux, BSD, MacOS X, MSYS and cygwin
function KillChilds
local pid="$1" # Parent pid to kill childs
local self="$2:-false" # Should parent be killed too ?
# Paranoid checks, we can safely assume that $pid should not be 0 nor 1
if [ $(IsInteger "$pid") -eq 0 ] || [ "$pid" == "" ] || [ "$pid" == "0" ] || [ "$pid" == "1" ]; then
echo "CRITICAL: Bogus pid given [$pid]."
return 1
fi
if kill -0 "$pid" > /dev/null 2>&1; then
# Warning: pgrep is not native on cygwin, must be installed via procps package
if children="$(pgrep -P "$pid")"; then
if [[ "$pid" == *"$children"* ]]; then
echo "CRITICAL: Bogus pgrep implementation."
children="$children/$pid/"
fi
for child in $children; do
KillChilds "$child" true
done
fi
fi
# Try to kill nicely, if not, wait 15 seconds to let Trap actions happen before killing
if [ "$self" == true ]; then
# We need to check for pid again because it may have disappeared after recursive function call
if kill -0 "$pid" > /dev/null 2>&1; then
kill -s TERM "$pid"
if [ $? != 0 ]; then
sleep 15
kill -9 "$pid"
if [ $? != 0 ]; then
return 1
fi
else
return 0
fi
else
return 0
fi
else
return 0
fi
function TrapQuit
local exitcode="$1:-0"
KillChilds $SCRIPT_PID > /dev/null 2>&1
exit $exitcode
# Launch TrapQuit on USR1 / other signals
trap TrapQuit USR1 QUIT INT EXIT
【讨论】:
以上是关于在 USR1 信号后可靠地杀死睡眠进程的主要内容,如果未能解决你的问题,请参考以下文章