Work the Shell - 处理信号

作者:Dave Taylor

本月,我认为从我通常耗时数月的编程项目中稍微偏离一下,转而关注一个对于编写较长脚本的人来说非常重要的特定主题会很有趣:信号管理。

信号是从操作系统、其他应用程序或用户发送到正在运行的应用程序的数字消息,它们通常会调用特定的响应,例如“优雅地关闭”、“停止运行以便我可以将您置于后台”或“死掉!”

很可能,您已经使用过 kill 命令向不同的程序发送信号,但是如果您曾经按下 Ctrl-C 或 Ctrl-Z 来停止正在运行的应用程序,那么您也向正在运行的应用程序发送了信号。

信号以级联方式管理。它被发送到应用程序或脚本,然后如果应用程序没有特定的处理程序(信号管理或响应函数),它将被推回 shell 或操作系统。某些信号无法在单个应用程序中管理,例如 SIGKILL,它会被操作系统捕获并立即杀死正在运行的应用程序(包括 shell:SIGKILL 您的登录 shell,您就退出了登录)。

为了开始这段旅程,让我们找出您的 Linux 版本可以处理哪些信号。通过键入以下内容来完成此操作kill -l(那是小写字母 L,而不是数字 1)

$ kill -l
1)  SIGHUP    2) SIGINT      3) SIGQUIT   4) SIGILL
5)  SIGTRAP   6) SIGABRT     7) SIGEMT    8) SIGFPE
9)  SIGKILL  10) SIGBUS     11) SIGSEGV  12) SIGSYS
13) SIGPIPE  14) SIGALRM    15) SIGTERM  16) SIGURG
17) SIGSTOP  18) SIGTSTP    19) SIGCONT  20) SIGCHLD
21) SIGTTIN  22) SIGTTOU    23) SIGIO    24) SIGXCPU
25) SIGXFSZ  26) SIGVTALRM  27) SIGPROF  28) SIGWINCH
29) SIGINFO  30) SIGUSR1    31) SIGUSR2

这些信号大多数都无趣。 比较酷的是 SIGHUP,它在“挂断”或用户注销时发送;SIGINT,这是一个简单的中断(通常是 Ctrl-C);SIGKILL,信号中的“极端偏见终止”;SIGTSTP,它是 Ctrl-Z;SIGCONT,应用程序从 shell 命令 fg 和 bg 接收到的信号,在 SIGTSTP 之后;SIGWINCH,用于窗口系统事件,例如窗口大小调整;以及 SIGUSR1 和 SIGUSR2,它们旨在用于进程间通信。

让我们编写一些代码来看看会发生什么,好吗? 信号是通过内置的“trap”捕获的,这些信号映射命令的通用格式如下例所示

trap 'echo "Ctrl-C Ignored" ' INT

我们如何在 shell 脚本中玩这个? 这是一个简单的方法

#!/bin/bash

trap 'echo " - Ctrl-C ignored" ' INT
while /usr/bin/true ; do
  sleep 30
done

exit 0

您注意到那里的无限循环了吗? 它几乎没有使用任何资源,因为大部分时间都花在“sleep”上,但是如果您不采取措施结束它,此脚本将永远运行下去,或者直到玛雅人的预言在两年后被证明是正确的——两者之一。

让我们看看一种更灵活的信号管理方式,通过创建 shell 脚本函数

sigquit()
{
   echo "signal QUIT received"
}

sigint()
{
   echo "signal INT received, script ending"
   exit 0
}

trap 'sigquit' QUIT
trap 'sigint'  INT
trap ':'       HUP      # ignore the specified signals

echo "test script started. My PID is $$"
while /usr/bin/true ; do
  sleep 30
done

然后从另一个终端窗口运行它,并向它发送一些信号。

现在,让我们启动该脚本,并观察当我们发送一些不同的信号时会发生什么

$ ./test.sh
test script started. My PID is 25309
signal QUIT received
signal INT received, script ending
$

完美! 要发送信号,请从另一个终端窗口执行以下命令

$ kill -HUP  25309
$ kill -QUIT 25309
$ kill -INT  25309

有了这个有用的脚本,让我们看看如何在 shell 脚本中处理更复杂的信号,例如 Ctrl-Z。

停止! 不要停止!

我将在此处创建一个场景,而不是仅仅进行智力练习。 在一个复杂的脚本中,您意识到您有一些段落需要忽略 TSTP 信号(SIGTSTP 或 Ctrl-Z 或信号编号 18),而另一些地方可以停止并重新启动。 这可以做到吗?

为了开始制定解决方案,我将创建一个函数,该函数不仅可以处理指定的信号,而且还可以在单次调用后禁用自身

sigtstp()
{
  echo "SIGTSTP received" > /dev/tty
  trap - TSTP
  echo "SIGTSTP standard handling restored"
}

调用trap - signal在脚本中的其他地方,您已经重置了该信号处理程序,因此如果我有以下行

trap 'sigtstp' TSTP

就在我不希望 Ctrl-Z 工作的部分之前,它将忽略第一个 Ctrl-Z,然后重置信号处理程序并在您第二次按下该键时按预期工作。

更有用的是忽略所有 Ctrl-Z 停止信号,直到您准备好处理它们,这可以通过极简主义的方式轻松完成

trap : TSTP  # ignore Ctrl-Z requests

然后,当您准备好再次接收它们时

trap - TSTP  # allow Ctrl-Z requests

实验表明,存在一些与 SIGTSTP 相关的奇怪终端缓冲问题,但是,如果您有一个具有输出的信号处理程序,请不要感到惊讶。 在这种特殊情况下,它不会显示出来,直到脚本退出。

读取配置文件

让我们看一个更实际的例子。 假设您有一个管理员脚本,它应该始终作为守护程序运行,但偶尔您想调整其配置文件并让它重新读取其设置(比杀死并重新启动它快得多)。

此外,让我们将 SIGUSR1 用于此任务,因为这是它的预期用途,因此我们以预期的方式使用内核的信号处理子系统。

读取配置文件可能像以下一样简单

. $config

(回想一下,使用 . 意味着在辅助文件中设置的任何变量都会影响当前 shell,而不是子 shell。 source 命令与 . 命令的作用相同。)

这是我们用来试验此功能的脚本

#!/bin/bash

config="our.config.file"
sigusr1()
{
  echo "(SIGUSR1: re-reading config file)"
  . $config
}

trap sigusr1 USR1       # catch -USR1 signal

echo "Daemon started. Assigned PID is $$"

. $config               # read it first time

while /usr/bin/true; do
  echo "Target number = $number"
  sleep 5
done

trap - USR1             # reset to be elegant

exit 0

我们将从包含以下内容的配置文件开始number=5,然后在 10-15 秒后,将其更改为number=1。 但是,在我们发送实际的 USR1 信号之前,该脚本只是继续运行,而没有意识到它已更改

$ ./test2.sh
Daemon started. Assigned PID is 25843
Target number = 5
Target number = 5
Target number = 5

同时,在另一个窗口中,我已经编辑了文件,所以我输入了这个命令

$ kill -USR1 25843

这是主脚本窗口中发生的事情

(SIGUSR1: re-reading config file)
Target number = 1
Target number = 1

酷,是吧?

我希望这篇关于 shell 脚本中信号处理的探索对您有所帮助。 在我研究这里的代码时,我实际上学到了很多关于高级处理的知识。 对于如何在捕获 SIGTSTP 后重置输出流,我仍然有点困惑,但我敢打赌,一些敏锐的 Linux Journal 读者会找到答案。

Dave Taylor 长期以来一直在破解 shell 脚本,已有 30 年了。 他是流行的 Wicked Cool Shell Scripts 的作者,可以在 Twitter 上通过 @DaveTaylor 找到他,更常见的情况是在 www.DaveTaylorOnline.com

加载 Disqus 评论