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

已发表: 2022-08-09
显示 bash 提示符的 Linux 笔记本电脑
fatmawati achmad zaenuri/Shutterstock.com

Linux 内核向进程发送有关它们需要做出反应的事件的信号。 表现良好的脚本优雅而稳健地处理信号,即使您按下 Ctrl+C 也可以自行清理。 就是这样。

信号和过程

信号是发送到脚本、程序和守护进程等进程的短而快速的单向消息。 他们让流程知道已经发生的事情。 用户可能按下了 Ctrl+C,或者应用程序可能试图写入它无权访问的内存。

如果进程的作者已经预料到某个信号可能会发送给它,他们可以在程序或脚本中编写一个例程来处理该信号。 这样的例程称为信号处理程序。 它捕获或捕获信号,并执行一些动作来响应它。

如何从 Linux 终端管理进程:你需要知道的 10 个命令
相关如何从 Linux 终端管理进程:你需要知道的 10 个命令

正如我们将看到的,Linux 使用了很多信号,但从脚本的角度来看,只有一小部分信号是您可能感兴趣的。特别是在非平凡的脚本中,信号告诉要关闭的脚本应该被捕获(在可能的情况下)并执行正常关闭。

例如,创建临时文件或打开防火墙端口的脚本可以有机会删除临时文件或在端口关闭之前关闭端口。 如果脚本在收到信号的那一刻就死了,您的计算机可能会处于不可预测的状态。

下面介绍如何在自己的脚本中处理信号。

满足信号

一些 Linux 命令具有神秘的名称。 捕获信号的命令并非如此。 这叫trap 。 我们还可以使用带有-l (列表)选项的trap来向我们显示 Linux 使用的整个信号列表。

 陷阱-l 

使用 trap -l 列出 Ubuntu 中的信号

尽管我们的编号列表以 64 结束,但实际上有 62 个信号。 信号 32 和 33 丢失。 它们没有在 Linux 中实现。 它们已被gcc编译器中用于处理实时线程的功能所取代。 从信号 34 SIGRTMIN到信号 64 SIGRTMAX的所有内容都是实时信号。

您会在不同的类 Unix 操作系统上看到不同的列表。 例如,在 OpenIndiana 上,存在信号 32 和 33,以及一堆额外的信号,使总数达到 73。

使用 trap -l 列出 OpenIndiana 中的信号

信号可以通过名称、编号或它们的简称来引用。 他们的简称就是他们的名字,去掉了前面的“SIG”。

发出信号的原因有很多。 如果你能破译它们,它们的目的就包含在它们的名字中。 信号的影响属于以下几类之一:

  • 终止:进程终止。
  • 忽略:信号不影响进程。 这是一个仅供参考的信号。
  • 核心:创建转储核心文件。 这通常是因为进程以某种方式越界,例如内存违规。
  • 停止:进程停止。 也就是说,它是暂停的,而不是终止的。
  • 继续:告诉停止的进程继续执行。

这些是您最常遇到的信号。

  • SIGHUP :信号 1. 与远程主机(例如 SSH 服务器)的连接意外断开或用户已注销。 接收到此信号的脚本可能会正常终止,或者可能选择尝试重新连接到远程主机。
  • SIGINT : Signal 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 ”格式说明符。

 陷阱 'echo -e "\nCtrl+c 检测到。"' SIGINT 

在命令行上捕获 Ctrl+C

每次按下 Ctrl+C 组合时,都会打印我们的文本行。

要查看是否在信号上设置了陷阱,请使用-p (打印陷阱)选项。

 陷阱 -p SIGINT 

检查是否在信号上设置了陷阱

使用不带选项的trap可以做同样的事情。

要将信号重置为其未捕获的正常状态,请使用连字符“ - ”和捕获信号的名称。

 陷阱 - SIGINT
 陷阱 -p SIGINT 

从信号中移除陷阱

trap -p命令没有输出表明该信号上没有设置陷阱。

在脚本中捕获信号

我们可以在脚本中使用相同的通用格式trap命令。 此脚本捕获三个不同的信号SIGINTSIGQUITSIGTERM

 #!/bin/bash

陷阱“回显我已终止 SIGINT;退出”SIGINT
陷阱“回显我被 SIGQUIT 终止;退出”SIGQUIT
陷阱“回显我被 SIGTERM 终止;退出”SIGTERM

回声$$
计数器=0

虽然是真的
做 
  echo "循环编号:" $((++counter))
  睡觉 1
完毕

三个trap语句位于脚本的顶部。 请注意,我们在每个信号的响应中都包含了exit命令。 这意味着脚本会对信号做出反应,然后退出。

将文本复制到编辑器中并将其保存在名为“simple-loop.sh”的文件中,并使用chmod命令使其可执行。 如果您想在自己的计算机上进行操作,则需要对本文中的所有脚本执行此操作。 只需在每种情况下使用相应脚本的名称即可。

 chmod +x simple-loop.sh 

使用 chmod 使脚本可执行

脚本的其余部分非常简单。 我们需要知道脚本的进程 ID,所以我们让脚本回显给我们。 $$变量保存脚本的进程 ID。

我们创建一个名为counter的变量并将其设置为零。

while循环将永远运行,除非它被强制停止。 它递增counter变量,将其回显到屏幕上,然后休眠一秒钟。

让我们运行脚本并向它发送不同的信号。

 ./simple-loop.sh 

标识它的脚本已被 Ctrl+C 终止

当我们点击“Ctrl+C”时,我们的消息会打印到终端窗口并且脚本会终止。

让我们再次运行它并使用kill命令发送SIGQUIT信号。 我们需要从另一个终端窗口执行此操作。 您需要使用您自己的脚本报告的进程 ID。

 ./simple-loop.sh
 杀死-SIGQUIT 4575 

标识它的脚本已被 SIGQUIT 终止

正如预期的那样,脚本报告信号到达然后终止。 最后,为了证明这一点,我们将使用SIGTERM信号再次执行此操作。

 ./simple-loop.sh
 杀死-SIGTERM 4584 

标识它的脚本已用 SIGTERM 终止

我们已经验证我们可以在一个脚本中捕获多个信号,并独立地对每个信号做出反应。 将所有这些从有趣变为有用的步骤是添加信号处理程序。

在脚本中处理信号

我们可以将响应字符串替换为脚本中的函数名称。 然后,当检测到信号时, trap命令会调用该函数。

将此文本复制到编辑器中并将其保存为名为“grace.sh”的文件,并使用chmod使其可执行。

 #!/bin/bash

陷阱 graceful_shutdown SIGINT SIGQUIT SIGTERM

优雅的关机()
{
  echo -e "\n删除临时文件:" $temp_file
  rm -rf "$temp_file"
  出口
}

temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX)
echo "创建的临时文件:" $temp_file

计数器=0

虽然是真的
做 
  echo "循环编号:" $((++counter))
  睡觉 1
完毕

该脚本使用单个trap语句为三个不同的信号( SIGHUPSIGINTSIGTERM )设置陷阱。 响应是graceful_shutdown()函数的名称。 每当接收到三个捕获信号之一时,都会调用该函数。

该脚本使用mktemp在“/tmp”目录中创建一个临时文件。 文件名模板是“tmp.XXXXXXXXXX”,所以文件名是“tmp”。 后跟十个随机字母数字字符。 文件名在屏幕上回显。

脚本的其余部分与前一个相同,带有一个counter变量和一个无限的while循环。

 ./grace.sh 

通过删除临时文件执行正常关闭的脚本

当文件被发送一个导致它关闭的信号时, graceful_shutdown()函数被调用。 这将删除我们的单个临时文件。 在实际情况下,它可以执行脚本所需的任何清理工作。

此外,我们将所有捕获的信号捆绑在一起,并用一个函数处理它们。 您可以单独捕获信号并将它们发送到它们自己的专用处理函数。

复制此文本并将其保存在名为“triple.sh”的文件中,并使用chmod命令使其可执行。

 #!/bin/bash

陷阱 sigint_handler SIGINT
陷阱 sigusr1_handler SIGUSR1
陷阱 exit_handler 退出

函数 sigint_handler() {
  ((++sigint_count))

  echo -e "\nSIGINT 收到 $sigint_count 次。"

  如果 [[ "$sigint_count" -eq 3 ]]; 然后
    echo "开始关闭。"
    loop_flag=1
  菲
}

函数 sigusr1_handler() {
  echo "SIGUSR1 发送和接收 $((++sigusr1_count)) 次。"
}

函数 exit_handler() { 
  echo "退出处理程序:脚本正在关闭..."
}

回声$$
sigusr1_count=0
sigint_count=0
loop_flag=0

而 [[ $loop_flag -eq 0 ]]; 做
  杀死-SIGUSR1 $$
  睡觉 1
完毕

我们在脚本顶部定义了三个陷阱。

  • 一个陷阱SIGINT并有一个名为sigint_handler()的处理程序。
  • 第二个捕获一个名为SIGUSR1的信号并使用一个名为sigusr1_handler()的处理程序。
  • 第三个陷阱捕获EXIT信号。 该信号在脚本关闭时由脚本本身发出。 为EXIT设置信号处理程序意味着您可以设置一个在脚本终止时始终调用的函数(除非它被信号SIGKILL杀死)。 我们的处理程序称为exit_handler()

SIGUSR1SIGUSR2是提供的信号,以便您可以向脚本发送自定义信号。 您如何解释和对它们做出反应完全取决于您。

暂时将信号处理程序放在一边,您应该熟悉脚本的主体。 它将进程 ID 回显到终端窗口并创建一些变量。 变量sigusr1_count记录了SIGUSR1被处理的次数, sigint_count记录了SIGINT被处理的次数。 loop_flag变量设置为零。

while循环不是无限循环。 如果loop_flag变量设置为任何非零值,它将停止循环。 while循环的每次旋转都使用killSIGUSR1信号发送到此脚本,方法是将其发送到脚本的进程 ID。 脚本可以向自己发送信号!

sigusr1_handler()函数增加sigusr1_count变量并向终端窗口发送消息。

每次收到SIGINT信号时, siguint_handler()函数都会增加sigint_count变量并将其值回显到终端窗口。

如果sigint_count变量等于 3,则loop_flag变量设置为 1,并向终端窗口发送一条消息,让用户知道关闭过程已经开始。

因为loop_flag不再等于 0,所以while循环终止并且脚本完成。 但是该操作会自动引发EXIT信号并exit_handler()函数。

 ./triple.sh 

使用 SIGUSR1 的脚本,需要三个 Ctrl+C 组合才能关闭,并在关闭时捕获 EXIT 信号

在按三下 Ctrl+C 后,脚本终止并自动调用exit_handler()函数。

阅读信号

通过捕获信号并在简单的处理程序函数中处理它们,即使 Bash 脚本意外终止,您也可以让它们自己整理。 这为您提供了一个更干净的文件系统。 它还可以防止您下次运行脚本时出现不稳定情况,并且根据脚本的用途,它甚至可以防止安全漏洞。

相关:如何使用 Lynis 审核 Linux 系统的安全性