在 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。

有关更多信息,请参阅:

您可能想要使用一个函数来终止包括子进程在内的整个进程树,尝试很好地终止它,如果 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