SIGALRM 定时器和 Stdin 分析

作者:Dave Taylor

创建函数以确保您的脚本不会永远运行并不难。但是,如果您希望某些部分计时,而其他部分可以根据需要运行多长时间呢?别着急,Dave 在他的最新“Work the Shell”专栏中解释道。

在之前的文章 中,我开始构建一个骨架脚本,该脚本将具有您可能想要创建的任何体面的 shell 脚本所需的基本功能。我从使用 getopts 处理命令行参数开始,然后探讨了 syslog 和状态日志作为脚本。最后,我在那一栏的结尾谈到了如何捕获像 Ctrl-C 这样的信号,并调用可以在实际放弃 shell 脚本控制权之前清理临时文件等的函数。

这一次,我想探讨 shell 脚本中信号管理的另一个方面:内置定时器,让您可以为特定函数或命令指定允许的时间配额来完成,并在其挂起时产生明确的后果。

命令何时挂起?通常是在您利用网络资源时。例如,您可能有一个脚本通过 curl 向 Google 发送查询来查找定义。如果一切运行正常,它会在一两秒钟内完成,然后您就可以继续了。

但是,如果网络离线或 Google 出现问题,或者网络查询可能失败的数百万个其他原因中的任何一个,您的脚本会发生什么?它会永远挂起,依赖 curl 程序本身具有超时功能吗? 这可不好。

报警定时器

最常见的报警定时器方法之一是给整个脚本一个特定的时间量,让它在该时间内完成,方法是生成一个子 shell,该子 shell 等待该配额,然后杀死其父进程。是的,有点像俄狄浦斯,但至少我们在这个脚本中没有戳瞎任何眼睛!

添加的行最终看起来像这样


(
sleep 600           # if 10 minutes pass
kill -TERM $$       # send it a SIGTERM signal
)&

没有涉及“trap”——很简单。特别注意,右括号有一个尾随的 & 符号,以确保子 shell 被推到后台运行,而不会阻止父脚本继续执行。

一种更智能、更简洁的方法是让定时器子 shell 向父进程发送适当的 SIGALRM 信号——一个小小的调整


(
sleep 600            # if 10 minutes pass
kill -ALRM $$        # send it a SIGALRM signal
)&

但是,如果您这样做,父脚本中需要什么来捕获 SIGALRM? 让我们添加它,并沿途设置一些函数,以继续为您的脚本添加有用的通用功能的这一主题


function allow_time
{
   ( echo timer allowing $1 seconds for execution
     sleep $1
     kill -ALRM $$
   ) &
}

第一个函数让您可以轻松地为后续执行设置时间,而第二个函数以更简洁的方式呈现您的 ALRM 处理程序


function timeout_handler
{
   echo allowable time for execution exceeded.
   exit 1
}

请注意,这两个脚本都具有调试输出,这可能不是实际生产代码所需要的。 它很容易被注释掉,但按原样运行它将帮助您了解事物如何交互和协同工作。

这可能如何使用? 像这样


trap timeout_handler SIGALRM
allow_time 10
code that has ten seconds to complete

这将给脚本十秒钟完成。

问题是,如果它在少于分配的时间内完成会发生什么? 子 shell 仍然在那里等待,并且它向一个不存在的进程发出信号,导致出现以下草率的错误消息


sigtest.sh: line 7: kill: (10532) - No such process

有两种方法可以解决这个问题,要么在父 shell 退出时杀死子 shell,要么让子 shell 在发送信号之前测试父 shell 的存在。

让我们做后者。 这更容易,并且让子 shell 在 sleep 中浮动几秒钟肯定不会浪费计算资源。

测试指定进程是否存在的最简单方法是使用 ps 并检查返回代码,如下所示


ps $$ >/dev/null ; echo $?

如果进程存在,则返回代码为 0。如果它消失了,则返回代码将为非零。 这表明一个简单的测试


if [ ! $(ps $$ > /dev/null) ]

但是,这行不通,因为它是返回代码,而不是传递给 shell 的内容。 解决方案? 只需调用 ps 命令,然后让表达式测试返回代码


function allow_time
{
   ( echo timer allowing $1 seconds for execution
     sleep $1
     ps $$ > /dev/null
     if [ ! $? ] ; then
       kill -ALRM $$
     fi
   ) &
}

这解决了这个问题。 但是,如果您有代码段,您希望限制其执行时间,然后是您不在乎的其他代码段怎么办?

如果您不介意留下一些子进程等待向父进程发送信号,那就很容易。 只需使用这个


trap '' SIGALRM

当您完成定时段落时。 发生的情况是定时器生成一个信号,但父脚本忽略它。

当然,这方面的限制是如果您有这样的代码


regular code
possible runaway code <-- allocate 100 seconds
cancel timer
more regular code
possible runaway code <-- allocate 100 seconds

如果第二个代码块在第一个定时器耗尽之前启动,则会出现这种情况。 想象一下,您为第一个定时块分配了 100 秒,它在 90 秒内完成。 常规代码需要五秒钟,然后您进入第二个块,正好十分钟。 然后第一个 ALRM 定时器触发,在十分钟后而不是另外 100 分钟后触发。 不好。

这确实有点极端情况,但为了解决这个问题,让我们反转关于子进程在发送信号之前测试父进程存在的决定,而是让父脚本在定时部分完成后杀死所有子 shell。 构建起来有点棘手,因为它需要使用 ps 并拾取比该子 shell 更多的进程,因此您不仅需要屏蔽掉您自己的进程,还需要摆脱任何实际上不是脚本本身的子 shell 进程。

我使用以下方法


ps -g $$ | grep $myname | cut -f1 -d\  | grep -v $$

这将生成所有正在运行的子 shell 的进程 ID (pid) 列表,然后您可以将其馈送到 kill


pids=$(ps -g $$ | grep $myname | cut -f1 -d\  | grep -v $$)
kill $pids

问题是,并非所有这些进程在被传递到 kill 程序时仍然存在。 解决方案? 忽略 PID 未找到产生的任何错误


kill $pids > /dev/null 2>&1

组合成一个函数,它看起来像这样


function kill_children
{
   myname=$(basename $0)
   pids=$(ps -g $$ | grep $myname | cut -f1 -d\  | grep -v $$)
   kill $pids > /dev/null 2>&1
}

如果您在想“天哪,同一个脚本中的多个定时器有点乱”,那您就对了。 在您需要这种性质的东西时,很可能不同的解决方案会是更明智的途径。

此外,我确信还有其他方法可以解决这个问题,在这种情况下,我非常有兴趣听取读者关于您是否遇到过需要代码的多个定时部分的情况,如果是,您是如何管理的! 通过 https://linuxjournal.cn/contact 发送电子邮件。

秒表图片 通过 Shutterstock.com。

Dave Taylor 长期以来一直在 UNIX 和 Linux 系统上编写 shell 脚本。 他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。 您可以在 Twitter 上找到他,用户名是 @DaveTaylor,您可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论