Bash Trap 命令
如果您编写过任何数量的 bash 代码,您可能遇到过trap命令。Trap 允许您捕获信号并在信号发生时执行代码。信号是异步通知,当某些事件发生时会发送到您的脚本。这些通知大多数是针对您希望永远不会发生的事件,例如无效的内存访问或错误的系统调用。但是,可能有一两个事件您可能想要合理地处理。还有可用的“用户”事件,这些事件永远不会由系统生成,您可以生成这些事件来向您的脚本发出信号。Bash 还提供了一个名为“EXIT”的伪信号,它会在您的脚本退出时执行;这可以用来确保您的脚本在退出时执行一些清理操作。
signal(7) 的手册页描述了所有可用的信号。signal (IPC) 的 Wikipedia 页面有更多细节。正如我提到的,它们中的大多数在脚本中都很少受到关注。“SIGINT”信号可能是脚本中唯一可能引起关注的信号。当您在键盘上键入 Ctrl-C 以中断正在运行的脚本时,会生成 SIGINT。如果您不希望您的脚本像这样被停止,您可以捕获该信号并提醒自己应该避免中断脚本。虽然,正如您将看到的,这不如人们希望的那么有用。“SIGUSR1”信号是一个“用户”定义的信号,您可以随意使用它。它永远不会由系统生成。
trap 命令最常见的用途是捕获 bash 生成的名为 EXIT 的伪信号。例如,假设您有一个脚本创建了一个临时文件。与其在脚本退出的每个位置删除它,不如在脚本的开头放置一个 trap 命令,该命令在退出时删除该文件
tempfile=/tmp/tmpdata
trap "rm -f $tempfile" EXIT
现在,每当您的脚本退出时,它都会删除您的临时文件。trap 命令的语法是“trap COMMAND SIGNALS...”,因此,除非您要执行的命令是单个单词,“command”部分应该用引号引起来。
如果您的清理需求很复杂,您不必尝试将所有内容都塞进带有分号的字符串中,只需编写一个函数即可
function cleanup()
{
# ...
}
trap cleanup EXIT
请注意,如果您发送kill -9到您的脚本,它将不会在退出之前执行 EXIT trap。
您可能希望使用 trap 命令的另一个可能的事情是捕获 Ctrl-C,以便您的脚本不会被中断,或者您可以询问用户是否真的想要中断该进程。作为一个例子,我将使用以下处理函数,它在前两次 Ctrl-C 时警告用户,然后在第三次时退出
ctrlc_count=0
function no_ctrlc()
{
let ctrlc_count++
echo
if [[ $ctrlc_count == 1 ]]; then
echo "Stop that."
elif [[ $ctrlc_count == 2 ]]; then
echo "Once more and I quit."
else
echo "That's it. I quit."
exit
fi
}
使用以下命令来测试处理程序
trap no_ctrlc SIGINT
while true
do
echo Sleeping
sleep 10
done
如果您运行它并键入 Ctrl-C 三次,您将得到以下输出
$ bash noctrlc.sh
Sleeping
^C
Stop that.
Sleeping
^C
Once more and I quit.
Sleeping
^C
That's it. I quit.
$
我对测试脚本的第一次尝试使用了sleep 10作为 while 条件
trap no_ctrlc SIGINT
while sleep 10
do
echo Sleeping
done
但这不起作用。经过一番思考,我意识到这是因为当 trap 命令返回时,它不会在中断的点恢复“sleep”命令,也不会重新启动“sleep”命令,而是返回到中断命令之后的下一个命令,在本例中,它是 while 循环之后的任何内容。所以循环结束,脚本正常退出。
这是一个重点:中断的命令不会重新启动。因此,如果您的脚本需要执行一些不应该被中断的重要操作,那么您不能,例如,使用 trap 命令来捕获信号,打印警告,然后像什么都没发生一样恢复操作。相反,如果您不能让某些东西被中断,您需要做的是在命令执行时禁用 Ctrl-C 处理。您也可以使用 trap 命令来执行此操作,方法是指定一个空命令来 trap。您也可以使用 trap 命令通过指定“-”作为命令来将信号处理重置为默认值。所以您可能会这样做
# Run something important, no Ctrl-C allowed.
trap "" SIGINT
important_command
# Less important stuff from here on out, Ctrl-C allowed.
trap - SIGINT
not_so_important_command
因此,除非您的脚本有长时间的等待,否则捕获信号并实际执行某些操作可能无法提供您期望的体验。
我想看的最后一件事是捕获发送到脚本的用户定义信号。假设我想监视系统日志并计算sudo运行的次数,并且我想在后台运行该脚本,然后在我想让它显示计数时向它发送一个信号
nopens=0
function show_opens()
{
echo "Seen $nopens sudo session opens"
}
trap show_opens USR1
sudo journalctl -f | while read line
do
if [[ $line =~ sudo.*session.*opened ]]; then
let nopens++
fi
done
这样做是将journalctl(即系统日志)的输出管道传输到循环中的read命令。在循环内部,if 语句检查该行是否是 sudo 命令。如果是,则递增计数器。循环之前的代码为 SIGUSR1 信号设置了一个 trap,当收到信号时,“show_opens”函数会打印出自脚本启动以来看到的 sudo 命令的数量。您可以使用 kill 命令向脚本发送 SIGUSR1 信号
$ bash bkgnd.sh &
[1] 1000
$ kill -SIGUSR1 1000
不幸的是,再一次,这未能奏效。我发现的第一个问题,我最近在我的关于作业控制的帖子中提到过,是如果 sudo 命令需要提示输入密码,则脚本会在启动后立即挂起。
提示:重置 sudo 的密码时间戳
如果您需要使用 sudo 测试某些内容,并想确保在 sudo 提示输入密码和不提示输入密码时一切正常工作,请执行命令sudo -k以重置 sudo 的密码时间戳。在使用 -k 选项执行 sudo 后,sudo 将在下次运行时再次要求输入密码,无论您最近输入密码的时间有多近。
在弄清楚挂起的后台问题后,我认为一切系统都“准备就绪”,但事实并非如此,仍然没有任何结果。现在的问题是因为循环正在从管道获取输入。原始 bash 进程现在为“journalctl”执行了一个子进程,为“while read line ...”执行了另一个子进程。当 bash 执行命令时,根据手册页
shell 捕获的 traps 会重置为从 shell 的父进程继承的值
因此,当这些子进程启动时,SIGUSR1 trap 会被重置,并且不再对该进程产生影响。要使其工作,我们需要在循环内设置 trap,使其成为子进程的一部分
nopens=0
function show_opens()
{
echo "Seen $nopens sudo session opens"
}
sudo journalctl -f | while read line
do
if [[ -z "$trap_set" ]]; then
trap_set=1
echo "Trap set in $BASHPID"
trap show_opens USR1
fi
if [[ $line =~ sudo.*session.*opened ]]; then
let nopens++
fi
done
请注意,我使用$BASHPID来获取子进程的进程 ID($$总是返回原始 shell 的进程 ID)。
现在它可以工作了
$ bash bkgnd.sh &
[1] 1000
Trap set in 1002
$ kill -SIGUSR1 1002
Seen 1 sudo session opens
$ sudo ls
...
$ kill -SIGUSR1 1002
Seen 2 sudo session opens
最后,我不能说我可能会经常捕获 SIGINT 或任何其他信号(EXIT 除外),但我可以说我在使这些示例工作的过程中发现了一些关于 bash 的有趣的细微之处。
# Copyright 2019 Mitch Frazier <mitch -at- linuxjournal.com>
#
# This software may be used and distributed according to the terms of the
# MIT License or the GNU General Public License version 2 (or any later version).