如何在 Bash 脚本中使用 Linux 信号

Linux 内核向进程发送有关它们需要响应的事件的信号。行为良好的脚本可以优雅而稳健地处理信号,并且即使您按下 Ctrl+C,也可以自行清理。就是这样。
信号和过程
信号是发送到脚本、程序和守护进程等进程的短而快速的单向消息。他们让流程知道已经发生的事情。用户可能按下了 Ctrl+C,或者应用程序可能试图写入它无权访问的内存。
如果进程的作者已经预料到某个信号可能会发送给它,他们可以在程序或脚本中编写一个例程来处理该信号。这样的例程称为信号处理程序。它捕获或捕获信号,并响应它执行一些操作。
正如我们将要看到的,Linux 使用了很多信号,但是从脚本的角度来看,您可能只对一小部分信号感兴趣。特别是在非平凡的脚本中,信号告诉应该捕获要关闭的脚本(如果可能)并执行正常关闭。
例如,可以为创建临时文件或打开防火墙端口的脚本提供删除临时文件或在它们关闭之前关闭端口的机会。如果脚本在收到信号的那一刻就死了,您的计算机可能会处于不可预测的状态。
以下是您如何在自己的脚本中处理信号。
满足信号
一些 Linux 命令具有神秘的名称。不是这样的陷阱信号的命令。它叫做 trap
。我们还可以使用带有 -l
(列表)选项的 trap
来向我们展示 Linux 使用的完整信号列表。
trap -l

虽然我们的编号列表以 64 结束,但实际上有 62 个信号。信号 32 和 33 丢失。它们未在 Linux 中实现。它们已被 gcc
编译器中用于处理实时线程的功能所取代。从信号 34 SIGRTMIN
到信号 64 SIGRTMAX
的所有内容都是实时信号。
你会在不同的类 Unix 操作系统上看到不同的列表。例如,在 OpenIndiana 上,存在信号 32 和 33,以及一堆额外的信号,使总数达到 73。

信号可以通过名称、编号或简称来引用。他们的简称只是他们的名字去掉了开头的“SIG”。
发出信号的原因有很多。如果你能破译它们,它们的目的就包含在它们的名字中。信号的影响分为以下几类之一:
- 终止:流程终止。
- 忽略:信号不影响进程。这是一个仅供参考的信号。
- 核心:创建转储核心文件。这样做通常是因为进程以某种方式违规,例如内存违规。
- 停止:进程停止。也就是说,它是暂停,而不是终止。
- 继续:告诉停止的进程继续执行。
这些是您最常遇到的信号。
- SIGHUP:信号 1。与远程主机(例如 SSH 服务器)的连接意外断开或用户已注销。接收此信号的脚本可能会正常终止,或者可能会选择尝试重新连接到远程主机。
- SIGINT:信号 2。用户按下 Ctrl+C 组合键强制关闭进程,或者
kill
命令已与信号 2 一起使用。技术上,这是一个中断信号,而不是终止信号,但没有信号处理程序的中断脚本通常会终止。 - SIGQUIT:信号 3。用户已按下 Ctrl+D 组合键强制进程退出,或者
kill
命令已与信号 3 一起使用。< /李> - SIGFPE:信号 8。进程试图执行非法(不可能的)数学运算,例如除以零。
- SIGKILL:信号 9。这是相当于断头台的信号。你无法抓住它或忽略它,它会立即发生。该过程立即终止。
- SIGTERM:信号 15。这是更体贴的
SIGKILL
版本。SIGTERM
也告诉进程终止,但它可能会被捕获并且进程可以在关闭之前运行其清理进程。这允许正常关机。这是kill
命令发出的默认信号。
命令行上的信号
捕获信号的一种方法是使用 trap
和信号的编号或名称,以及在收到信号时希望发生的响应。我们可以在终端窗口中演示这一点。
此命令捕获 SIGINT
信号。响应是将一行文本打印到终端窗口。我们将 -e
(启用转义)选项与 echo
一起使用,因此我们可以使用“\n
”格式说明符。
trap 'echo -e "\nCtrl+c Detected."' SIGINT

每次我们按下 Ctrl+C 组合时,我们的文本行都会被打印出来。
要查看是否在信号上设置了陷阱,请使用 -p
(打印陷阱)选项。
trap -p SIGINT

使用没有选项的 trap
做同样的事情。
要将信号重置为其未捕获的正常状态,请使用连字符“-
”和捕获信号的名称。
trap - SIGINT
trap -p SIGINT

trap -p
命令没有输出表明没有在该信号上设置陷阱。
在脚本中捕获信号
我们可以在脚本中使用相同的通用格式 trap
命令。此脚本捕获三种不同的信号,SIGINT
、SIGQUIT
和 SIGTERM
。
#!/bin/bash
trap "echo I was SIGINT terminated; exit" SIGINT
trap "echo I was SIGQUIT terminated; exit" SIGQUIT
trap "echo I was SIGTERM terminated; exit" SIGTERM
echo $$
counter=0
while true
do
echo "Loop number:" $((++counter))
sleep 1
done
三个 trap
语句位于脚本的顶部。请注意,我们已将 exit
命令包含在对每个信号的响应中。这意味着脚本对信号做出反应然后退出。
将文本复制到您的编辑器中并将其保存在名为“simple-loop.sh”的文件中,并使用 chmod
命令使其可执行。如果您想在自己的计算机上继续操作,则需要对本文中的所有脚本执行此操作。在每种情况下只需使用适当脚本的名称。
chmod +x simple-loop.sh

脚本的其余部分非常简单。我们需要知道脚本的进程 ID,所以我们让脚本回显给我们。 $$
变量保存脚本的进程 ID。
我们创建一个名为 counter
的变量并将其设置为零。
while
循环将永远运行,除非它被强行停止。它递增 counter
变量,将其回显到屏幕上,然后休眠一秒钟。
让我们运行脚本并向它发送不同的信号。
./simple-loop.sh

当我们按下“Ctrl+C”时,我们的消息被打印到终端窗口并且脚本被终止。
让我们再次运行它并使用 kill
命令发送 SIGQUIT
信号。我们需要从另一个终端窗口执行此操作。您需要使用您自己的脚本报告的进程 ID。
./simple-loop.sh
kill -SIGQUIT 4575

正如预期的那样,脚本报告信号到达然后终止。最后,为了证明这一点,我们将用 SIGTERM
信号再做一次。
./simple-loop.sh
kill -SIGTERM 4584

我们已经验证了我们可以在一个脚本中捕获多个信号,并独立地对每个信号做出反应。将所有这些从有趣变成有用的步骤是添加信号处理程序。
在脚本中处理信号
我们可以用脚本中的函数名称替换响应字符串。 trap
命令随后会在检测到信号时调用该函数。
将此文本复制到编辑器中并将其保存为名为“grace.sh”的文件,并使用 chmod
使其可执行。
#!/bin/bash
trap graceful_shutdown SIGINT SIGQUIT SIGTERM
graceful_shutdown()
{
echo -e "\nRemoving temporary file:" $temp_file
rm -rf "$temp_file"
exit
}
temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX)
echo "Created temp file:" $temp_file
counter=0
while true
do
echo "Loop number:" $((++counter))
sleep 1
done
该脚本使用单个 trap
语句为三种不同的信号(SIGHUP
、SIGINT
和 SIGTERM
)设置陷阱.响应是 graceful_shutdown()
函数的名称。只要接收到三个捕获信号之一,就会调用该函数。
该脚本使用 mktemp
在“/tmp”目录中创建一个临时文件。文件名模板为“tmp.XXXXXXXXXX”,因此文件名将为“tmp”。后跟十个随机字母数字字符。文件名在屏幕上回显。
脚本的其余部分与前一个脚本相同,具有一个 counter
变量和一个无限的 while
循环。
./grace.sh

当向文件发送导致其关闭的信号时,将调用 graceful_shutdown()
函数。这将删除我们的单个临时文件。在现实世界中,它可以执行您的脚本需要的任何清理工作。
此外,我们将所有被捕获的信号捆绑在一起,并用一个函数处理它们。您可以单独捕获信号并将它们发送到它们自己的专用处理函数。
复制此文本并将其保存在名为“triple.sh”的文件中,并使用 chmod
命令使其可执行。
#!/bin/bash
trap sigint_handler SIGINT
trap sigusr1_handler SIGUSR1
trap exit_handler EXIT
function sigint_handler() {
((++sigint_count))
echo -e "\nSIGINT received $sigint_count time(s)."
if [[ "$sigint_count" -eq 3 ]]; then
echo "Starting close-down."
loop_flag=1
fi
}
function sigusr1_handler() {
echo "SIGUSR1 sent and received $((++sigusr1_count)) time(s)."
}
function exit_handler() {
echo "Exit handler: Script is closing down..."
}
echo $$
sigusr1_count=0
sigint_count=0
loop_flag=0
while [[ $loop_flag -eq 0 ]]; do
kill -SIGUSR1 $$
sleep 1
done
我们在脚本的顶部定义了三个陷阱。
- 一个陷阱
SIGINT
并有一个名为sigint_handler()
的处理程序。 - 第二个捕获一个名为
SIGUSR1
的信号并使用一个名为sigusr1_handler()
的处理程序。 - 第三个陷阱捕获
EXIT
信号。该信号在脚本关闭时由脚本本身引发。为EXIT
设置信号处理程序意味着您可以设置一个在脚本终止时始终调用的函数(除非它被信号SIGKILL
终止)。我们的处理程序称为exit_handler()
。
SIGUSR1
和 SIGUSR2
是提供的信号,以便您可以将自定义信号发送到您的脚本。你如何解释和回应他们完全取决于你。
现在先把信号处理程序放在一边,您应该熟悉脚本的主体。它将进程 ID 回显到终端窗口并创建一些变量。变量sigusr1_count
记录了SIGUSR1
被处理的次数,sigint_count
记录了SIGINT
被处理的次数。 loop_flag
变量设置为零。
while
循环不是无限循环。如果 loop_flag
变量设置为任何非零值,它将停止循环。 while
循环的每次旋转都使用 kill
将 SIGUSR1
信号发送到该脚本,方法是将其发送到脚本的进程 ID。脚本可以向自己发送信号!
sigusr1_handler()
函数递增 sigusr1_count
变量并将消息发送到终端窗口。
每次接收到 SIGINT
信号时,siguint_handler()
函数都会增加 sigint_count
变量并将其值回显到终端窗口。
如果 sigint_count
变量等于 3,则 loop_flag
变量设置为 1,并向终端窗口发送一条消息,让用户知道关闭过程已开始。
因为 loop_flag
不再等于零,所以 while
循环终止并且脚本完成。但是该操作会自动引发 EXIT
信号并调用 exit_handler()
函数。
./triple.sh

按三下 Ctrl+C 后,脚本终止并自动调用 exit_handler()
函数。
阅读信号
通过捕获信号并在简单的处理程序函数中处理它们,即使 Bash 脚本意外终止,您也可以让它们自行清理。这给了你一个更干净的文件系统。它还可以防止您下次运行脚本时出现不稳定,并且——根据脚本的用途——它甚至可以防止安全漏洞。