在脚本中使用 trap

捕捉信号

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

David Tansley, 系统管理员, Ace Europe

//ibm.com/developerworks/i/p-dtansley.jpgDavid Tansley 是一位自由作家。他有 15 年 UNIX 系统管理经验,最近 8 年使用 AIX。他喜欢打羽毛球和观赏一级方程式赛车,但是最喜欢与妻子一起开着 GSA 摩托车旅行。


developerWorks 投稿作者

2011 年 8 月 17 日

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

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

清单 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
fi
 
# copy  over updated 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

在该演示中,我使用了 bash v3.2。bash shell 可以从 AIX Toolbox 下载,参见 参考资料 小节。

在使用 清单 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

获得信号

清单 1清单 2 中包含的两个例子展示了检查是否可以执行后续命令的一种方法。但是,如果脚本在执行过程中终止,该怎么办呢?可以通过信号机制终止脚本(注意,并不是所有发送的信号都是终止信号)。向正在运行的进程发送信号会中断该进程并强制执行某种事件,通常是某种操作。信号源包括但不仅限于以下内容:

  • 内核或用户空间,通过执行某些系统事件发出信号。
  • 进程本身,通过键盘发出信号 (Ctrl-C)。
  • 进程发出的某个非法指令。
  • 另一个进程,通过另一个用户向进程发送一个终止命令 (kill) 发出信号。
  • 通知,通过通知某个必要设备的状态发出信号。

要查看当前信号列表,使用 -l (字母 l)命令。下面的表格显示了这一列表(信号编号,信号名称):

 $ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
 5) SIGTRAP      6) SIGABRT      7) SIGEMT       8) SIGFPE
 9)SIGKILL     10) SIGBUS      11) SIGSEGV     12) SIGSYS
…....
…....

要查看信号及其(在 AIX 机器上的)默认操作,请查看以下文件:

$ cat /usr/include/sys/signal.h|more
…..
…..
#define SIGHUP     1    /* hangup, generated when terminal disconnects */
#define SIGINT     2    /* interrupt, generated from terminal special char */
#define SIGQUIT    3    /* (*) quit, generated from terminal special char */
#define SIGILL     4    /* (*) illegal instruction (not reset when caught)*/
#define SIGTRAP    5    /* (*) trace trap (not reset when caught) */
#define SIGABRT    6    /* (*) abort process */
…..
…..

我收到了一个信号,然后该怎么做?

脚本接收到信号后,它可以采取以下三种操作之一:

  1. 忽略该信号,不执行任何操作。如果脚本编写者没有理解该信号,那么大部分脚本通常都会采取这一操作。
  2. 使用 trap 捕捉信号并采取相应的操作。
  3. 采取默认操作。

除了下面的信号外,以上操作均适用:

SIGKILL(信号 9)

SIGSTOP (信号 17)

SIGCONT(信号 19)

这些信号是无法捕捉的,并且始终采取默认操作。SIGKILL 总是会终止进程。查看 /usr/include/sys/signal.h 文件中的清单,我们可以看到每个信号的默认操作。例如,SIGINT(信号 2)是由终端产生的中断;通常,终端是指键盘。每个已定义的系统信号都有不同的操作。还有两个由用户定义的信号:SIGUSR1 (信号 30)和 SIGUSR2(信号 31)。

在收到信号后,将由脚本人员写者采取必要的操作(如果有的话)。

脚本编写人员可以通过该清单执行预定信号。请务必查看 signal.h 文件,了解所有默认操作。

常见信号包括:

  • SIGHUP:从终端终止或退出正在前台运行的进程
  • SIGINT:从键盘按下 Ctrl-C
  • SIGQUIT:从键盘按下 Ctrl-\
  • SIGTERM :软件终止信号

在接收到信号后,可以采取的操作包括:

  • 清除文件
  • 提示用户是否应当终止脚本
  • 忽略该信号
  • 进行处理

捕捉信号

要捕捉发送到您的进程的信号,请使用内置的 trap 命令。在捕捉到信号后,正在执行的当前命令会尝试在 trap 接管之前结束执行。如果该命令为 SIGKILL,那么终止将立即执行。如果忽略某些信号,将执行默认操作。例如,如果只对 SIGINT 执行 trap 命令,但是对 SIGQUIT 不执行任何操作,然后您的进程捕捉到了 SIGQUIT 信号,那么将执行默认操作(很可能是终止您的脚本,而这正是您不希望看到的)。

trap 命令的格式为:

trap 'command_list'  signals

其中,command_list 是一个命令清单,可以包含一个函数,在接收到信号列表中包含的某个信号后运行。而 signals 是将要捕捉的信号的列表。

要忽略某个信号,使用两个单引号代替 command_list:

trap ''  signals

要重置 trap,使用:

trap - signals

其中,signals 为信号列表。

现在,让我们来看一个只包含基本内容的脚本,它将捕捉 SIGINTSIGQUIT 信号。下面清单 3 中包含的脚本是一个计数器迭代脚本。当用户按下键盘中的 Ctrl-C 或 Ctrl-\ 组合时,trap 命令会捕捉信号,并发回一条消息,指出脚本已终止。终止是通过在命令列表的末尾使用 exit 命令完成的。如果没有执行这些操作,那么脚本并不会终止,并且将继续执行。在本例中,我们希望终止脚本。在有些情形下可能不希望终止脚本,并且应当继续进行相应处理。

清单 3. Trap1
#!/bin/bash
# trap1
trap 'echo you hit Ctrl-C/Ctrl-\, now exiting..; exit' SIGINT SIGQUIT
count=0

while :
 do
   sleep 1
   count=$(expr $count + 1)
   echo $count
 done

$ trap1
1
2
3
^Cyou hit Ctrl-C/Ctrl-\, now exiting..

在 trap 命令中使用信号名而不是信号号码被认为是一种好方法。这样做是为了便于跨其他系统进行移植。

您还可以使用一个函数替代该命令,如清单 4 所示:

清单 4. Trap1a
#!/bin/bash
# trap1a
trap 'my_exit; exit' SIGINT SIGQUIT
count=0

my_exit()
{
echo "you hit Ctrl-C/Ctrl-\, now exiting.."
 # cleanp commands here if any
}

while :
 do
   sleep 1
   count=$(expr $count + 1)
   echo $count
 done

在后台运行脚本时,仍然可以捕获信号。下面的清单 5 包含一个与前面示例相同的计数器。在接下来的例子中,我将再次选择在捕捉到信号后退出脚本。如果这是一个文件处理脚本,那么应首先删除掉已创建的临时文件。

该脚本将通过以下命令提交到后台:

$ /home/dxtans/trapbg &
[1] 708790
$ 1
2
3

现在,从另一个终端上发送信号 SIGHUP 来终止脚本。

$ ps -ef |grep trapbg
 dxtans 708790 2457860 11:49:39 pts/0 0:00 /bin/bash /home/dxtans/trapbg
$ kill -1 708790

回到在其上提交脚本的终端,将显示如下所示的内容:

$ /home/dxtans/trapbg &
[1] 708790
$ 1
2
3
Going down on a SIGHUP - signal 1, now exiting..
[1]+ Done   /home/dxtans/trapbg
清单 5. trapbg
#!/bin/bash
# trapbg
trap 'echo Going down on a SIGHUP - signal 1, now exiting..; exit' SIGHUP
count=0
while :
do
 sleep 10
 count=$(expr $count + 1)
 echo $count
done

在处理信号时,最常见的任务是清除临时文件。通常,这些文件是使用 PID (脚本流程 pid)创建的,PID 被附加到 /tmp 中用户创建的文件上。假设临时文件的形式为:

hold1.$$
hold2.$$

移除这些文件的常见命令为:

rm /tmp/hold*.$$

下面的代码将捕捉 SIGNHUP SIGINT SIGQUIT SIGTERM,然后移除文件:

trap 'rm /tmp/hold*.$$; exit' SIGNHUP SIGINT SIGQUIT SIGTERM

在本文前面,我演示了使用 set -e 时,脚本会在收到来自命令的非零退出状态时终止。在 trap 内部,您可以使用一个类似的选项;该选项实际上并不是信号,但是基于 set -e,因此看上去就好像是调用了 set -e 一样。它从命令中捕捉到一个非零的退出状态,并使用了 ERR 变量。ERR 在 trap 命令中提供了一个信号列表。在后面的例子中,将复制一个并不存在的文件,这会导致出现错误:

#!/bin/bash
# trap1b
trap 'echo I have error in my script..' ERR
cp /home/dxtans/afile /tmp

在执行示例代码后,输出如下:

$ trap1b
cp: /home/dxtans/afile: A file or directory in the path name does not exist.
I have error in my script.

在处理 trap 以获得更多有关脚本终止的信息时,有两个变量可供您很方便地使用,它们分别为 LINENOBASH_COMMANDBASH_COMAMND 只限于 bash。这两个变量将报告或尝试报告脚本当前执行的行号,以及正在运行的命令。下面的清单 6 给出的例子展示了这一点。脚本将执行一组 echo 和 sleep 命令。当脚本接收到 SIGHUP, SIGINT, SIGQUIT 时,会终止运行。在捕捉到 trap 后,会显示一条包含行号和命令的消息;脚本随后将退出执行(使用 trap 命令列表中的 exit 命令)。注意,trap 调用函数 my_exit 来显示信息。通过解析参数 $1(LINENO)和 $2(BASH_COMMAND),它还将一条消息记录到事件的 /var/adm/messages 中。如果需要,其他清除命令也将被放入该函数中。

清单 6. trap4
#!/bin/bash
# trap4

trap 'my_exit $LINENO $BASH_COMMAND; exit' SIGHUP SIGINT SIGQUIT
my_exit()
{
echo "$(basename $0)  caught error on line : $1 command was: $2"

logger -p notice "script: $(basename $0) was terminated: line: $1, command was $2"
 # cleanp commands here if any
}

echo 1
sleep 1
echo 2
sleep 1
echo 3

多次运行该脚本,然后相隔不同的时间中断运行,这将生成以下输出:

$ trap4
1
2
^Ctrap4  caught error on line : 15 command was: sleep

$ trap4
1
^Ctrap4  caught error on line : 13 command was: sleep

在 /var/adm/messages 中,包含一条用于脚本终止的条目:

Apr  6 12:12:46 rs6000 user:notice dxtans: script: trap4 was terminated: line: 13,
 command was sleep

在有些情形下,您需要忽略某些信号。也许当您的脚本对大型文件进行处理时,您希望避免由于不小心按下 Ctrl-C 或 Ctrl-\ 键的情况,并且您希望在没有用户中断的情况下完成脚本。下面的代码片段实现了这个目标:

trap '' SIGINT SIGQUIT

您还可以在执行脚本的某个部分时忽略某些信号,然后在您的确希望捕捉信号时重新使用这些信号,从而可以采取某些措施。下面清单 7 中的脚本忽略了信号 SIGINT 和 SIGQUIT,直到 sleep 命令完成。然后,当下一个 sleep 命令启动时,trap 会在发送信号后生效,然后终止。与前面的例子一样,可以假设 sleep 命令表示某种形式的处理。

清单 7. trapoff_on
#!/bin/bash
# trapoff_on

trap '' SIGINT SIGQUIT
echo "you cannot terminate using ctrl-c or ctrl-\, "
# heavy pressing go on here, cannot interrupt !
sleep 10

trap 'echo terminated; exit' SIGINT SIGQUIT
# user can now interrupt
echo "ok you can now terminate me using those keystrokes"
sleep 10

向子进程发送信号

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

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

下面的例子将在父进程接收到信号时终止子进程。

清单 8. 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..

脚本终止后,未返回任何内容:

# ps -ef |grep sleep

结束语

在脚本中使用 trap 需要额外花费一些功夫。最终,当某个可以捕获的信号进入到脚本时,您便可以主动地采取相应措施。

参考资料

学习

  • 参考 AIX v7 文档中心
  • AIX and UNIX 专区:developerWorks 的“AIX and UNIX 专区”提供了大量与 AIX 系统管理的所有方面相关的信息,您可以利用它们来扩展自己的 UNIX 技能。
  • AIX and UNIX 新手入门:访问“AIX and UNIX 新手入门”页面可了解更多关于 AIX 和 UNIX 的内容。
  • AIX and UNIX 专题汇总:AIX and UNIX 专区已经为您推出了很多的技术专题,为您总结了很多热门的知识点。我们在后面还会继续推出很多相关的热门专题给您,为了方便您的访问,我们在这里为您把本专区的所有专题进行汇总,让您更方便的找到您需要的内容。
  • AIX and UNIX 下载中心:在这里你可以下载到可以运行在 AIX 或者是 UNIX 系统上的 IBM 服务器软件以及工具,让您可以提前免费试用他们的强大功能。
  • IBM Systems Magazine for AIX 中文版:本杂志的内容更加关注于趋势和企业级架构应用方面的内容,同时对于新兴的技术、产品、应用方式等也有很深入的探讨。IBM Systems Magazine 的内容都是由十分资深的业内人士撰写的,包括 IBM 的合作伙伴、IBM 的主机工程师以及高级管理人员。所以,从这些内容中,您可以了解到更高层次的应用理念,让您在选择和应用 IBM 系统时有一个更好的认识。

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=752930
ArticleTitle=在脚本中使用 trap
publish-date=08172011