Shell 防止SIGINT在传递有关SIGINT的信息时中断当前任务(并保留退出代码)

Shell 防止SIGINT在传递有关SIGINT的信息时中断当前任务(并保留退出代码),shell,signals,sh,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: '$re

我有一个相当长的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)”
在本例中)被中断。 我该怎么做才能让它表现得更像我被设置了陷阱“2”

我正在寻找一个POSIX解决方案,或者是一个受大多数shell解释器(BusyBox、dash、Cygwin…)支持的解决方案


我已经看到了答案,但这对我来说并不奏效。所有这些解决方案都需要修改不应中断的每条线路。我的真实脚本相当长,比示例复杂得多。我必须修改数百行。

首先需要防止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访问它并提前终止它。请注意,我们已经在这里打开了至少两个新的蠕虫罐头。通过在循环中等待,我们实际上忽略了子命令可能引发的任何错误(我们这样做是为了尝试并实现SIGRESTART),因此可能会挂起。此外,如果SIGINT在
cat
期间到达,我们试图通过在后台运行来阻止
cat
中止,但现在变量赋值将终止,您将获得原始行为。外壳内的信号处理不干净!但这会使您更接近您想要的目标。

在shell脚本中进行Sighandling可能会变得笨拙。这几乎是不可能的 在没有C语言支持的情况下“正确”地完成它

问题在于:

result="$(sleep 2 ; echo success)" # run some program
就是说,
$()
创建一个子shell,在子shell中,不忽略(
trap''信号是如何忽略信号的)
信号被重置为其默认配置,
SIGINT
将终止该过程 (
$()
获取自己的进程,但由于终端生成了SIGINT,因此它也将接收信号 是否以流程组为目标)

要防止出现这种情况,您可以执行以下操作:

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

但如前所述,在比赛开始之间将有一个小的比赛条件窗口 子shell和信号处理程序的注册
子shell可能会被重置为默认信号终止。

来自@PSkocik和@williampersell的两个答案都帮助我走上了正确的轨道

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

#!/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中的此代码存在一些问题

有关信号的主要功能正常工作。 然而,似乎完成的后台进程并没有作为僵尸留在系统中。这使得
等待$不工作。脚本的退出代码具有不正确的值
127

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

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

#!/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等。

关于不允许中断程序:

陷阱“”错误HUP INT退出术语TSTP TTIN TTOU

但如果子命令自己处理陷阱,并且该命令必须真正完成,则需要防止向其传递信号

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

[命令]

或者,您可以根据需要将修改后的应用程序,或者使用中的代码。尽管这样做的缺点是不能从上游的更新中获益


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

您是否可以在后台运行命令<代码>睡眠2;回显成功
?这有可能吗?@伊尼安说,这解决不了问题。例如,在执行
echo“Cleaning up…”
(粘贴真正的Cleaning up)的过程中,仍然可以发送SIGINT,这将更加糟糕。这是一个非常糟糕的解决方案。如果用户在清理过程中按Ctrl+C怎么办?我知道它会被忽略,但这一点都不好。这将是一个糟糕的用户界面。我会一边修改
!等待do:;完成类似于while的
操作!等待做睡眠1;完成
。一个简单繁忙的循环会大大降低脚本的速度,特别是在我的脚本中,它是m
#!/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'"