在 USR1 信号后可靠地终止睡眠进程
Reliably kill sleep process after USR1 signal
我正在编写一个 shell 脚本,它会定期执行任务并在从另一个进程接收到 USR1 信号时执行。
脚本的结构类似于this answer:
#!/bin/bash
trap 'echo "doing some work"' SIGUSR1
while :
do
sleep 10 && echo "doing some work" &
wait $!
done
但是,这个脚本有一个问题,睡眠进程在后台继续,只有在超时时才会结束。 (请注意,在 wait $! 期间收到 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 并打印错误。更何况,我不喜欢这个代码。
有没有更好的方法在中断时可靠地杀死分叉进程?或者实现相同功能的替代结构?
由于后台作业是前台作业的分支,因此它们具有相同的名称(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,我需要将后台作业放入 一个单独的进程组 — 这需要作业控制启用,否则所有作业都将放入主 shell 的进程组,如上面 pstree
的输出所示—,并向其发送 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
将 return 并且循环将继续。
如果您改用 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.
您可能想要使用一个函数来终止包括子进程在内的整个进程树,尝试很好地终止它,如果 niceness 不起作用则强制终止它。
这是您可以添加到脚本中的部分。
TrapQuit 在 SIGUSR1 或收到的其他退出信号(包括 CTRL+C)上被调用。
您可以在 TrapQuit 中添加任何需要的处理,或者在带有退出代码的普通脚本退出时调用它。
# Kill process and children bash 3.2+ implementation
# BusyBox compatible version
function IsInteger {
local value=""
#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="" # 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
我正在编写一个 shell 脚本,它会定期执行任务并在从另一个进程接收到 USR1 信号时执行。
脚本的结构类似于this answer:
#!/bin/bash
trap 'echo "doing some work"' SIGUSR1
while :
do
sleep 10 && echo "doing some work" &
wait $!
done
但是,这个脚本有一个问题,睡眠进程在后台继续,只有在超时时才会结束。 (请注意,在 wait $! 期间收到 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 并打印错误。更何况,我不喜欢这个代码。
有没有更好的方法在中断时可靠地杀死分叉进程?或者实现相同功能的替代结构?
由于后台作业是前台作业的分支,因此它们具有相同的名称(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,我需要将后台作业放入 一个单独的进程组 — 这需要作业控制启用,否则所有作业都将放入主 shell 的进程组,如上面 pstree
的输出所示—,并向其发送 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
将 return 并且循环将继续。
如果您改用 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.
您可能想要使用一个函数来终止包括子进程在内的整个进程树,尝试很好地终止它,如果 niceness 不起作用则强制终止它。 这是您可以添加到脚本中的部分。
TrapQuit 在 SIGUSR1 或收到的其他退出信号(包括 CTRL+C)上被调用。 您可以在 TrapQuit 中添加任何需要的处理,或者在带有退出代码的普通脚本退出时调用它。
# Kill process and children bash 3.2+ implementation
# BusyBox compatible version
function IsInteger {
local value=""
#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="" # 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