在 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 信号后可靠地杀死睡眠进程的主要内容,如果未能解决你的问题,请参考以下文章

通过信号SIGQUIT唤醒睡眠守护进程

Nginx进程信号管理

Linux kill 命令 以及USR1 信号解释

使用信号从睡眠中唤醒进程

错误编译进程 SIGALRM kill

使用极光/友盟推送,APP进程杀死后为啥收不到推送