【问题标题】:Reliably kill sleep process after USR1 signal在 USR1 信号后可靠地杀死睡眠进程
【发布时间】: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 并打印一个错误。更何况,我不喜欢这段代码。

有没有更好的方法可以在被中断时可靠地杀死分叉的进程?或者是实现相同功能的替代结构?

【问题讨论】:

    标签: linux bash shell sleep bash-trap


    【解决方案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。

    有关详细信息,请参阅:

    【讨论】:

      【解决方案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
      

      【讨论】:

        猜你喜欢
        • 2023-03-26
        • 1970-01-01
        • 2023-02-01
        • 2015-06-11
        • 2016-08-20
        • 2015-11-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多