Shell脚本:如何防止SIGINT中断当前任务

时间:2018-05-27 12:13:15

标签: shell signals sh

我有一个很长的shell脚本,我试图为它添加信号处理。

该脚本的主要任务是运行各种程序,然后清理它们的临时文件。

我想捕获SIGINT。 当捕获到信号时,脚本应该等待当前程序完成执行,然后进行清理并退出。

这是一个MCVE:

#!/bin/sh

stop_this=0
trap 'stop_this=1' 2

while true ; do
    result="$(sleep 2 ; echo success)" # run some program
    echo "result: '$result'"
    echo "Cleaning up..." # clean up temporary files
    if [ $stop_this -ne 0 ] ; then
        echo 'OK, time to stop this.'
        break
    fi
done

exit 0

预期结果:

Cleaning up...
result: 'success'
Cleaning up...
^Cresult: 'success'
Cleaning up...
OK, time to stop this.

实际结果:

Cleaning up...
result: 'success'
Cleaning up...
^Cresult: ''
Cleaning up...
OK, time to stop this.

问题是当前正在运行的指令(在这种情况下为result="$(sleep 2 ; echo success)")被中断。 我能做什么,它会更像我设置trap '' 2

我正在寻找POSIX解决方案或大多数shell解释器支持的解决方案(BusyBox,dash,Cygwin ......)

我已经看到了Prevent SIGINT from closing child process in bash script的答案,但这对我来说并不合适。所有这些解决方案都需要修改不应该被中断的每条线路。我的真实剧本很长,比例子复杂得多。我将不得不修改数百行。

5 个答案:

答案 0 :(得分:2)

您需要首先阻止SIGINT转到echo(或者重写您在变量赋值中运行的cmd以忽略SIGINT)。此外,您需要允许变量赋值发生,并且看起来shell在收到SIGINT时正在中止赋值。如果您只担心来自tty的用户生成的SIGINT,则需要将该命令与tty取消关联(例如,将其从前台进程组中取出)并防止SIGINT中止分配。你可以(几乎)完成以下任何一项:

#!/bin/sh

stop_this=0

while true ; do
    trap 'stop_this=1' INT
    { sleep 1; echo success > tmpfile; } & # run some program
    while ! wait; do : ; done
    trap : INT
    result=$(cat tmpfile& wait)
    echo "result: '$result'"
    echo "Cleaning up..." # clean up temporary files
    if [ $stop_this -ne 0 ] ; then
        echo 'OK, time to stop this.'
        break
    fi
done

exit 0

如果你担心来自其他来源的SIGINT,你将不得不重新实现sleep(或我认为sleep是代理的任何命令)以你想要的方式处理SIGINT 。这里的关键是在后台运行命令并等待它以防止SIGINT转到它并提前终止它。请注意,我们在这里至少打开了2个新的蠕虫病毒。通过循环等待,我们实际上忽略了子命令可能引发的任何错误(我们这样做是为了尝试并实现SIGRESTART),因此可能会挂起。此外,如果SIGINT在cat期间到达,我们尝试通过在后台运行来阻止cat中止,但现在变量赋值将被终止并且您将获得原始行为。外壳中的信号处理不干净!但这会让你更接近你想要的目标。

答案 1 :(得分:1)

shell脚本中的Sighandling可能会变得笨拙。这几乎是不可能的 没有C的支持,这样做“正确”。

问题:

result="$(sleep 2 ; echo success)" # run some program

$()创建子shell并且在子shell中,不可忽略(trap '' SIGNAL是你忽略SIGNAL的方式) 信号被重置为其默认配置,SIGINT将终止该过程 ($( )得到自己的进程,认为它也会收到信号,因为终端生成的SIGINT  是流程组目标)

为了防止这种情况,您可以执行以下操作:

result="$(
trap '' INT #ignore; could get killed right before the trap command
sleep 2; echo success)"

result="$( trap : INT; #no-op handler; same problem
sleep 2; while ! echo success; do :; done)" 

但是如上所述,在开始之间会有一个小的竞争条件窗口 subshel​​l和信号处理程序的注册期间 子shell可能会被重置为默认的SIGINT信号所杀死。

答案 2 :(得分:0)

来自@PSkocik和@WilliamPursell的答案都帮助我走上正轨。

我有一个完全可行的解决方案。它不漂亮,因为它需要使用外部文件来指示信号没有发生,但除此之外它应该可靠地工作。

#!/bin/sh

touch ./continue
trap 'rm -f ./continue' 2

( # the whole main body of the script is in a separate background process
trap '' 2 # ignore SIGINT
while true ; do
    result="$(sleep 2 ; echo success)" # run some program
    echo "result: '$result'"
    echo "Cleaning up..." # clean up temporary files
    if [ ! -e ./continue ] ; then # exit the loop if file "./continue" is deleted
        echo 'OK, time to stop this.'
        break
    fi
done
) & # end of the main body of the script
while ! wait ; do : ; done # wait for the background process to end (ignore signals)
wait $! # wait again to get the exit code
result=$? # exit code of the background process

rm -f ./continue # clean up if the background process ended without a signal

exit $result

编辑:Cygwin中此代码存在一些问题。

有关信号工作的主要功能。 但是,似乎完成的后台进程不会像僵尸一样留在系统中。这使wait $!不起作用。脚本的退出代码具有不正确的127值。

解决方法是删除行wait $!result=$?result=$?,以便脚本始终返回0。 也可以通过使用另一层子shell来保留正确的错误代码,并将退出代码临时存储在文件中。

答案 3 :(得分:0)

您可能需要调整以下内容:

#!/bin/sh

tmpfile=".tmpfile"
rm -f $tmpfile

trap : INT

# put the action that should not be interrupted in the innermost brackets
#         |                                 |
( set -m; (sleep 10; echo success > $tmpfile) & wait ) &
wait # wait will be interrupted by Ctrl+c

while [ ! -r $tmpfile ]; do
    echo "waiting for $tmpfile"
    sleep 1
done
result=`cat $tmpfile`
echo "result: '$result'"

这似乎也可以与安装自己的SIGINT处理程序(例如mpirun和mpiexec等)的程序一起使用。

答案 4 :(得分:0)

禁止中断程序:

<块引用>

trap "" ERR HUP INT QUIT TERM TSTP TTIN TTOU

但是如果一个子命令自己处理陷阱,并且该命令必须真正完成,你需要防止向它传递信号。

对于不介意安装额外命令的 Linux 用户,您可以使用:

<块引用>

waitFor [命令]

或者,您可以根据需要将 latest source code of waitFor 调整到您的程序中,或者使用 Gilles' answer 中的代码。尽管这样做的缺点是无法从上游更新中受益。

请注意,其他终端和服务管理器仍然可以终止“命令”。如果您希望服务管理器无法关闭“命令”,则应将其作为具有适当终止模式和终止信号集的服务运行。