trap 基本介绍

本文对trap做了简单的介绍,对网上搜到的知识点进行搬运,可能存在错误疏漏,后续会不断的补充修正。

用途说明

trap是一个shell内建命令,它用来在脚本中指定信号如何处理。比如,按Ctrl+C会使脚本终止执行,实际上系统发送了SIGINT信号给脚本进程,SIGINT信号的默认处理方式就是退出程序。如果要在Ctrl+C不退出程序,那么就得使用trap命令来指定一下SIGINT的处理方式了。trap命令不仅仅处理Linux信号,还能对脚本退出(EXIT)、调试(DEBUG)、错误(ERR)、返回(RETURN)等情况指定处理方式。

信号

信号是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号)。应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。进程收到一个信号后,会检查对该信号的处理机制。如果是SIG_IGN,就忽略该信号;如果是SIG_DFT,则会采用系统默认的处理动作,通常是终止进程或忽略该信号;如果给该信号指定了一个处理函数(捕捉),则会中断当前进程正在执行的任务,转而去执行该信号的处理函数,返回后再继续执行被中断的任务。

在有些情况下,我们不希望自己的shell脚本在运行时刻被中断,比如说我们写得shell脚本设为某一用户的默认shell,使这一用户进入系统后只能作某一项工作,如数据库备份, 我们可不希望用户使用Ctrl+C之类便进入到shell状态,做我们不希望做的事情。这便用到了信号处理。

信号源包括但不仅限于以下内容:
- 内核或用户空间,通过执行某些系统事件发出信号。
- 进程本身,通过键盘发出信号(Ctrl+C)。
- 进程发出的某个非法指令。
- 另一个进程,通过另一个用户向进程发送一个终止命令(kill)发出信号。
- 通知,通过通知某个必要设备的状态发出信号。
以下是一些你可能会遇到的,要在程序中使用的更常见的信号:

信号名称信号数描述
SIGHUP1本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。 登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
SIGINT2程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl C)时发出。
SIGQUIT3和SIGINT类似, 但由QUIT字符(通常是Ctrl /)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGFPE8在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
SIGKILL9用来立即结束程序的运行. 本信号不能被阻塞, 处理和忽略。
SIGALRM14时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号。
SIGTERM15程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理. 通常用来要求程序自己正常退出. shell命令kill缺省产生这个信号。

trap 命令格式

trap [-lp] [[arg] sigspec …]

格式:trap “commands” signals | trap ‘commands’ signals
当shell接收到signals指定的信号时,执行commands命令,commands是一个命令清单,可以包含一个函数。(The command arg is to be read and executed when the shell receives signal(s) sigspec. )

另外,在trap语句中,单引号和双引号是不同的,当shell程序第一次胖到trap语句时,将把commands中的命令扫描一边。此时若commands是用单引号括起来的话,那么shell不会对commands中的变量和命令进行替换,否则commands中的变量和命令将用当时具体的值来替换。

格式:trap signals 或 trap - signals
如果没有指定命令部分,那么就将信号处理复原。比如 trap INT 就表明恢复Ctrl+C退出。(If arg is absent (and there is a single sigspec) or -, each specified signal is reset to its original disposition (the value it had upon entrance to the shell). )

格式:trap “” signals
忽略信号signals,可以多个,比如 trap “” INT 表明忽略SIGINT信号,按Ctrl+C也不能使脚本退出。又如 trap “” HUP 表明忽略SIGHUP信号,即网络断开时也不能使脚本退出。(If arg is the null string the signal specified by each sigspec is ignored by the shell and by the commands it invokes. )

格式:trap -p
格式:trap -p signal
把当前的trap设置打印出来。(If arg is not present and -p has been supplied, then the trap commands associated with each sigspec are displayed. If no arguments are supplied or if only -p is given, trap prints the list of commands associated with each signal.)

格式:trap -l
把所有信号打印出来。(The -l option causes the shell to print a list of signal names and their corresponding numbers. Each sigspec is either a signal name defined in signal.h, or a signal number. Signal names are case insensitive and the SIG prefix is optional.)

这里写图片描述
此图在CentOS系统下的执行结果,不同系统下执行结果有差异。

格式:trap “commands” EXIT
脚本退出时执行commands指定的命令。(If a sigspec is EXIT (0) the command arg is executed on exit from the shell.)

格式:trap “commands” DEBUG
在脚本执行时打印调试信息,比如打印将要执行的命令及参数列表。(If a sigspec is DEBUG, the command arg is executed before every simple command, for command, case command, select command, every arithmetic for command, and before the first command executes in a shell function (see SHELL GRAMMAR above). Refer to the description of the extdebug option to the shopt builtin for details of its effect on the DEBUG trap.)

格式:trap “commands” ERR
当命令出错,退出码非0,执行commands指定的命令。(If a sigspec is ERR, the command arg is executed whenever a simple command has a non-zero exit status, subject to the following conditions. The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a && or ┅Ι│ list, or if the command’s return value is being inverted via !. These are the same conditions obeyed by the errexit option.)

格式:trap “commands” RETURN
当从shell函数返回、或者使用source命令执行另一个脚本文件时,执行commands指定的命令。(If a sigspec is RETURN, the command arg is executed each time a shell function or a script executed with the . or source builtins finishes executing. Signals ignored upon entry to the shell cannot be trapped or reset. Trapped signals that are not being ignored are reset to their original values in a child process when it is created. The return status is false if any sigspec is invalid; otherwise trap returns true.)

1.condition 的标准格式是什么?

sigspec 中的新高到底应该如何书写?比如终端中断信号(一般用CTRL+C触发),到底是写SIGINT、INT还是2(大部分系统上该信号对应的数字)?是大写还是小写?

如果你使用的是最新版的Bash,那么这几种写法都可以。而如果你需要在不同的shell中保持可移植性,请使用大写、不带前缀的INT!根据POSIX标准,trap 的sigspec不应当加上SIG前缀,且必须全大写,允许带SIG前缀或小写是某些shell的扩展功能。而信号数在不同的系统上可能不同,所以也不是一个好主意。

2. trap必须放在第一行么?

许多资料,尤其是中文资料中不容申辩地指明–trap必须放在脚本中第一行非注释行。事实果真如此么?

不论是manage还是POSIX文档汇总,我都没有找到任何与之相关的说明。甚至在TLDP的Bash Guid for Beginners中,多个例子都分明把trap放在了脚本的中间。最后我在这边文档中找到了下面这句经常被误读的话:

Normally, all traps are set before other executable code in the shell
script is encountered, i.e., at the beginning of the shell script.

果然,这只是一个为了保证信号钩子尽早被设立的一个设计管理罢了。事实上,trap可以根据你的需要放在脚本中的任何位置。脚本中也可以有多个trap,可以为不同的信号定义不同的行为,或是修改、删除已定义的trap。更进一步地,trap也有作用范围,你可以把它放在函数中,它将只在这个函数里起效!你看,其实trap的行为时很符合UNIX的管理的。

3. 信号究竟实在什么时候被trap处理?
信号到底是和啥呢么时候被处理的?更准确的说,比如脚本正在执行某个命令时收到了某个信号,那么它会被立即处理,还是要等待当前命令完成?

#!/bin/bash
trap 'echo "signal received!"; exit' INT
sleep 100

我们先写一段程序做了个实验。在bash中执行它,然后按CTRL+C发现程序立即停止了,然后打印了signal received,似乎trap是类似c程序一样立即处理信号的。但其实是被表面现象蒙骗了。

我们在此执行这个shell程序,然后打开另一个shell,在里面执行 kill -SIGINT xxx 发现shell程序并没有退出,等待了100秒之后才打印signal received退出。

因为第一次在键盘上按CTRL+C后,sleep程序和shell程序同属于一个进程组,所以也接到了int信号退出了,而第二次只有shell程序收到信号,所以造成了两者的差异。我第一次也被蒙骗了,傻乎乎的以为trap能在sleep时也处理信号。

这篇文档给了我们一个更准确的说明——如果当前正有一个外部命令在前台执行,那么 trap 会等待当前命令结束以后再处理信号队列中的信号。(而许多教程出错的另一个原因就是——某些 shell 中 sleep 是内建命令,会被打断。可以使用type sleep 命令查看是否是内置命令。)

那么问题来了,我们如果要在sleep时处理信号,并且及时退出怎么办呢?国外一篇文章给了例子,我就负责搬运一下了。

trap '[[ $pid ]] && kill $pid'  EXIT
sleep 10000 & 
pid=$!
wait 

利用了bash 的builtin命令wait。wait是一个shell内部的命令,而不是一个外部程序,所以它没有前面的限制。另外,要记得退出时kill掉sleep进程。

上文的例子应该要如何写才能达到想要的效果呢?有两种办法:1.把sleep放到后台进行,再用内建的wait去等待其执行结束;2.暴力一点,把一长段sleep拆成一秒的小sleep的循环,这在对精度要求不高的程序下也是一个可行的办法。

#!/bin/bash
trap "command" signal
cmd1 &
wait

要使脚本具有合理的健壮性,需要满足的条件之一就是能够清除强制终止后留下的任何临时日志或进程。另一项需要考虑的因素是,在收到来自用户的中断后,应当采取哪些相应措施?通过使用shell内置trap命令和logger工具,这些工具有助于提高脚本在被强制终止时的健壮性。在本文中。

在编写脚本时,一种良好的实践就是控制脚本的退出;这要考虑脚本处理中出现的失败条件。请考虑这样一个脚本:该脚本复制或替换文件系统中的某些文件。在继续执行脚本中的下一个任务之前,需要检查每次复制是否成功。如果出现问题,则脚本将退出。这循序系统管理员检查脚本出现故障的位置,从而能够立刻采取措施来退出该过程,或通过采取其他备用措施来完成任务。

下面清单1包含实现这一目标的基本条件代码。以文件复制进程为例,执行了一个测试来确保文件run_pi确实存在。如果该文件存在,那么可以通过复制或得目标文件的备份。如果复制失败,那么脚本将推出并显示一条信息,对错误进行详细说明。如果文件不存在,那么脚本将退出,并且不应执行任何处理。如果复制成功,那么将复制新的更新后的文件并覆盖旧文件,如果不成功,脚本退出。

清单1. Example_replace

#!/bin/bash
#
proj_dir=/opt/pcake/bin
#check file is present
if [ ! -f "$proj_dir/run_pj" ]
then
 echo " $proj_dir/run_pj not present ... exiting"
 exit 1
fi
#make a backup copy
cp -p $proj_dir/run_pj $proj_dir/run_pj.24042011
if [ $? !=0 ]
then
  echo "$proj_dir/run_pj no backup made ....exiting"
  exit 1

#copy over updatd file
if [ ! -f "/opt/dump/rollout/run_pj" ]
then
  echo "/opt/dump/rollout/run_pj not present...exiting"
  exit 1
fi
cp -p /opt/dump/rollout/run_pj $proj_dir/run_pj
if [ $? != 0 ]
then
  echo "$proj_dir/run_pj was not copied ... exiting"
  exit 1
fi

在使用清单1中的方法时,如果复制过程发生任何错误,那么脚本将退出,以阻止脚本在出现错误后继续执行。显然,应在重新运行脚本之前修复所有错误。

另一个检查错误并退出的技巧是使用set选项:

set -e

使用set选项-e:如果命令失败(即返回一个非零的退出状态),那么脚本将退出(除非是迭代&&,||命令的一部分)。下面清单2展示了的例子复制了一个不存在的文件。这里使用了set -e选项。如果复制命令失败,脚本将退出。注意,当您运行该命令时,实现最终的退出状态的if语句将永远无法满足条件, 以为脚本在遇到cp命令的非零返回状态时将退出。

清单2.Example_fail

#!/bin/bash
set -e
proj_dir=/opt/rollout/v12
#copy  a non-existent file
cp $proj_dir/go_sup /usr/local/bin/go_sup
if [  $? != 0 ]
then 
  echo "could not copy $proj_dir/go_sup to /usr/local/bin/"
  exit 1
fi

$cp_test
cp: /opt/rollout/v12/go_sup: A file or directory in the path name does not exist.

生成syslog消息
在使用logger命令时,允许shell和脚本通过syslogd服务将消息写入系统信息文件中。可以在脚本中使用这种方法来记录错误或完成进程,使所有查询信息文件的人都能够看到该错误。因此,您以及其他系统管理员就会在脚本生成事件时收到通知。

该命令最基本的格式为:

logger -p priority message

其中 -p 表示syslog中包含的优先级或设备级别(facility level)。
例如,下面的记录器命令包含调用脚本名(本例中为“rollout”)以及消息something has happened。

logger -p notice "$(basename $0) - something has happened"

以下输出将出现在/var/adm/messages中:

Apr  5 13:20:30 uk01wrs6008 user:notice dxtans:rollout -something has happened

向子进程发送信号

还需要处理包含子进程的脚本。假设您希望终止任意子进程,那么还需要停止这些脚本。可以通过下面清单3所示的 trap 命令完成此操作。在本例中,两个 sleep 命令均被用作子进程。这些进程都可以放到后台;在运行每个进程后,都会将进程的 PID 放到变量 $pid 中。该变量保存了子(休眠)进程的两个 PID。

要停止主脚本,请发送 SIGHUP,SIGINT,SIGQUIT 或 SIGTERM 信号。捕捉到该信号后,系统会向 $pid 变量中包含的子进程的 PID 发出一条 kill 命令。脚本完成执行之后就会退出。脚本末尾的 wait 命令会等待子进程终止或结束。有时可能需要更多的信号捕捉,它们包含在子脚本中,用于在退出前执行清理工作。显然,这取决于您的处理类型。

下面的例子将在父进程接收到信号时终止子进程。
清单3.trapchild

#!/bin/bash
# trapchild

sleep 120 &

pid="$!"

sleep 120 &
pid="$pid $!"

echo "my process pid is: $$"
echo "my child pid list is: $pid"

trap 'echo I am going down, so killing off my processes..; kill $pid; exit' SIGHUP SIGINT 
 SIGQUIT SIGTERM 

wait

完成脚本执行后,将显示下面的内容:

$ /home/dxtans/trap/trapchild
my process pid is: 6553626
my child pid list is: 5767380 6488072

查看正在运行进程和子进程(使用两个 sleep 命令)的终端。

$ ps -ef |grep trapchild
    root 6553626 5439516   0 20:51:32  pts/1  0:00 /bin/bash /home/dxtans/trap/trapchild
$ ps -ef |grep sleep
root 5767380 6553626   0 20:51:32  pts/1  0:00 sleep 120
root 6488072 6553626   0 20:51:32  pts/1  0:00 sleep 120

现在,向父进程发送一个 SIGTERM。脚本将终止,同时还将终止子进程。

$ kill -15 6553626

脚本终止后将生成如下输出:

$ /home/dxtans/trap/trapchild
my process pid is: 6553626
my child pid list is: 5767380 6488072
I am going down, so killing off my processes..
  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值