在 USR1 信号后可靠地终止睡眠进程

jam*_*meh 5 linux bash shell sleep bash-trap

我正在编写一个 shell 脚本,它定期执行任务并从另一个进程接收到 USR1 信号。

脚本的结构类似于这个答案

#!/bin/bash

trap 'echo "doing some work"' SIGUSR1

while :
do
    sleep 10 && echo "doing some work" &
    wait $!
done
Run Code Online (Sandbox Code Playgroud)

但是,此脚本存在睡眠过程在后台继续并且仅在超时时终止的问题。(请注意,当在等待期间收到 USR1 时,睡眠进程会在其常规超时期间徘徊,但周期性回声确实被取消。)例如,您可以使用pkill -0 -c sleep.

我读了这个页面,它建议在陷阱动作中杀死挥之不去的睡眠,例如

#!/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
Run Code Online (Sandbox Code Playgroud)

但是,如果我们快速向 USR1 信号发送垃圾邮件,则此脚本会出现竞争条件,例如:

pkill -USR1 trap-test.sh; pkill -USR1 trap-test.sh
Run Code Online (Sandbox Code Playgroud)

然后它会尝试杀死一个已经被杀死的 PID 并打印一个错误。更不用说,我不喜欢这段代码。

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

ogu*_*ail 5

由于后台作业是前台作业的一个分支,它们共享相同的名称 ( 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
Run Code Online (Sandbox Code Playgroud)

因此,让我们从头开始并在终端中重现主要问题。

$ 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)
Run Code Online (Sandbox Code Playgroud)

我禁用了作业控制以部分模拟非交互式 shell 的行为。

杀死后台作业并没有杀死sleep,我需要手动终止它。发生这种情况是因为发送到进程的信号不会自动广播到其目标的子进程;即sleep根本没有收到 TERM 信号。

为了杀死sleep子shell和子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
Run Code Online (Sandbox Code Playgroud)

这将打印my PID is xxx(其中xxx是前台进程的 PID)并开始循环。向xxx(ie kill -USR1 xxx)发送 USR1 信号将触发陷阱并导致后台进程及其子进程终止。因此wait将返回并且循环将继续。

如果您pkill改用它,它无论如何都会工作,因为后台进程会忽略 USR1。

有关更多信息,请参阅: