Linux内核向进程发送关于它们需要响应的事件的信号。行为良好的脚本可以优雅而稳健地处理信号,即使您按下Ctrl+C,也可以自行清理。下面是方法。
信号和过程
信号是发送到脚本、程序和守护程序等进程的简短、快速、单向消息。他们让过程知道已经发生的事情。用户可能按下了Ctrl+C,或者应用程序可能试图写入它无权访问的内存。
如果流程的作者预期可能会向其发送某个信号,他们可以将例程写入程序或脚本以处理该信号。这样的例程称为信号处理程序。它捕获或捕获信号,并对其执行一些操作。
正如我们将看到的那样,Linux使用了很多信号,但从脚本的角度来看,您可能只对信号的一小部分感兴趣。特别是,在非平凡的脚本中,告诉脚本关闭的信号应该被捕获(如果可能的话),并执行正常的关闭。
例如,可以为创建临时文件或打开防火墙端口的脚本提供删除临时文件或关闭端口的机会。如果脚本在收到信号的瞬间就死掉了,您的计算机可能会处于不可预知的状态。
以下是如何在自己的脚本中处理信号。
满足信号
有些Linux命令的名称很神秘。捕获信号的命令并非如此。这叫陷阱。我们还可以使用带有-l(list)选项的trap来显示Linux使用的整个信号列表。
trap -l
虽然我们的编号列表以64结束,但实际上有62个信号。信号32和33缺失。它们没有在Linux中实现。它们已经被gcc编译器中处理实时线程的功能所取代。从信号34 SIGRTMIN到信号64 SIGRTMAX都是实时信号。
您将在不同的类Unix操作系统上看到不同的列表。例如,在OpenIndiana上,存在信号32和33,以及一系列额外的信号,使总数达到73。
信号可以通过名称、编号或其缩写来引用。他们的简称只是他们的名字,去掉了前面的“SIG”。
发出信号有许多不同的原因。如果你能破译它们,它们的目的就包含在它们的名字中。信号的影响分为以下几类:
- 终止:进程已终止。
- 忽略:信号不影响进程。这是一个仅供参考的信号。
- 核心:创建转储核心文件。这通常是因为进程以某种方式越界,例如内存越界。
- 停止:进程已停止。也就是说,它是暂停的,而不是终止的。
- 继续:告诉停止的进程继续执行。
这些是你最常遇到的信号。
- SIGHUP:信号1。与远程主机(如SSH服务器)的连接意外断开或用户已注销。接收此信号的脚本可能会正常终止,或选择尝试重新连接到远程主机。
- 信号:信号2。用户按下Ctrl+C组合以强制关闭进程,或者kill命令与信号2一起使用。从技术上讲,这是一个中断信号,不是终止信号,但没有信号处理程序的中断脚本通常会终止。
- SIGQUIT:信号3。用户按下Ctrl+D组合以强制退出进程,或者kill命令与信号3一起使用。
- SIGFPE:信号8。进程试图执行非法(不可能的)数学运算,例如被零除。
- 信号9。这是相当于断头台的信号。你不能抓住它或忽略它,它会立即发生。进程立即终止。
- SIGTERM:Signal 15。这是SIGKILL更贴心的版本。SIGTERM还告诉进程终止,但它可能会被捕获,进程可以在关闭之前运行清理进程。这允许正常关机。这是kill命令发出的默认信号。
命令行上的信号
捕获信号的一种方法是使用带有信号编号或名称的陷阱,以及接收到信号时希望发生的响应。我们可以在终端窗口中演示这一点。
此命令捕获SIGINT信号。响应是将一行文本打印到终端窗口。我们将-e(启用转义)选项与echo一起使用,因此可以使用“\n”格式说明符。
trap 'echo -e "\nCtrl+c Detected."' SIGINT
每次按Ctrl+C组合键时,都会打印文本行。
要查看是否在信号上设置了陷阱,请使用-p(打印陷阱)选项。
trap -p SIGINT
在没有选项的情况下使用陷阱也有同样的作用。
要将信号重置为未捕获的正常状态,请使用连字符“-”和捕获信号的名称。
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
这三个陷阱语句位于脚本的顶部。注意,我们在对每个信号的响应中都包含了exit命令。这意味着脚本对信号作出反应,然后退出。
将文本复制到编辑器中,并将其保存在名为“simple loop.sh”的文件中,然后使用chmod命令使其可执行。如果您想在自己的计算机上继续学习,那么您需要对本文中的所有脚本都这样做。只需在每种情况下使用适当脚本的名称。
chmod +x simple-loop.sh
脚本的其余部分非常简单。我们需要知道脚本的进程ID,所以脚本会将其回传给我们。$$变量保存脚本的进程ID。
我们创建一个名为counter的变量,并将其设置为零。
除非强制停止,否则while循环将永远运行。它增加计数器变量,将其回显到屏幕上,然后休眠一秒钟。
让我们运行脚本并向其发送不同的信号。
./simple-loop.sh
当我们点击“Ctrl+C”时,我们的消息被打印到终端窗口,脚本被终止。
让我们再次运行它,并使用kill命令发送SIGQUIT信号。我们需要从另一个终端窗口执行此操作。您需要使用您自己的脚本报告的进程ID。
./simple-loop.sh
kill -SIGQUIT 4575
正如预期的那样,脚本报告信号到达,然后终止。最后,为了证明这一点,我们将再次使用SIGTERM信号。
./simple-loop.sh
kill -SIGTERM 4584
我们已经验证了我们可以在一个脚本中捕获多个信号,并对每个信号单独作出反应。将所有这些从有趣到有用的步骤是添加信号处理程序。
处理脚本中的信号
我们可以用脚本中函数的名称替换响应字符串。然后,陷阱命令在检测到信号时调用该函数。
将此文本复制到一个编辑器中,并将其保存为一个名为“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
该脚本使用单个陷阱语句为三种不同的信号(SIGHUP、SIGINT和SIGTERM)设置陷阱。响应是graceful_shutdown()函数的名称。每当接收到三个捕获信号中的一个时,就会调用该函数。
该脚本使用mktemp在“/tmp”目录中创建一个临时文件。文件名模板是“tmp.XXXXXXXXXX”,因此文件名为“tmp.”,后跟十个随机字母数字字符。文件名将在屏幕上回显。
脚本的其余部分与前一个相同,有一个计数器变量和一个无限的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()变量,并向终端窗口发送消息。
每次接收到SIGINT信号时,siguint_handler()函数都会递增SIGINT_count变量,并将其值回显到终端窗口。
如果sigint_count变量等于3,则loop_flag变量设置为1,并向终端窗口发送一条消息,让用户知道关闭过程已开始。
由于loop_flag不再等于零,while循环终止,脚本完成。但该操作会自动发出EXIT信号,并调用EXIT_handler()函数。
./triple.sh
按三次Ctrl+C组合键后,脚本终止并自动调用exit_handler()函数。
读取信号
通过捕获信号并在简单的处理程序函数中处理它们,您可以使Bash脚本自行整理,即使它们意外终止。这将为您提供一个更干净的文件系统。它还可以防止下次运行脚本时出现不稳定,并且根据脚本的用途,它甚至可以防止安全漏洞。
相关:如何使用Lynis审核Linux系统的安全性