Shell脚本:Linux Shell脚本学习指南(第三部分Shell高级)四

十九、Linux Shell trap命令:捕获信号

到目前为止,我们在本教程所见的脚本中还没有需要信号处理功能的,因为它们的内容相对比较简单,执行时间很短,而且不会创建临时文件。而对于较大的或者更复杂的脚本来说,如果脚本具有信号处理机制可能就比较有用了。

当我们设计一个大且复杂的脚本时,考虑到当脚本运行时出现用户退出或系统关机会发生什么是很重要的。当这样的事件发生时,一个信号将会发送到所有受影响的进程。相应地,这些进程的程序可以采取一些措施以确保程序正常有序地终结。比如说,我们编写了一个会在执行时生成临时文件的脚本。在好的设计过程中,我们会让脚本在执行完成时删除这些临时文件。同样聪明的做法是,如果脚本接收到了指示程序将提前结束的信号,也应删除这些临时文件。

接下来,就让我们开始学习,如何在脚本中进行这些处理。

trap 命令

Bash Shell 的内部命令 trap 让我们可以在 Shell 脚本内捕获特定的信号并对它们进行处理。 trap 命令的语法如下所示:

trap command signal [ signal ... ]

上述语法中,command 可以是一个脚本或是一个函数。signal 既可以用信号名,也可以用信号值指定。

你可以不指定任何参数,而直接使用 trap 命令,它将会打印与每个要捕获的信号相关联的命令的列表。

当 Shell 收到信号 signal(s) 时,command 将被读取和执行。比如,如果 signal 是 0 或 EXIT 时,command 会在 Shell 退出时被执行。如果 signal 是 DEBUG 时,command 会在每个命令后被执行。

signal 也可以被指定为 ERR,那么每当一个命令以非 0 状态退出时, command 就会被执行(注意,当非 0 退出状态来自一个 if 语句部分,或来自 while、until 循环时,command 不会被执行)。

下面我们通过几个简单的实例来学习 trap 命令的用法。

首先,我们定义一个变量 FILE:

[c.biancheng.net]$ FILE=`mktemp -u /tmp/testtrap.$$.XXXXXX`

这里使用 mktemp 命令创建一个临时文件;使用-u选项,表示并不真正创建文件,只是打印生成的文件名;“XXXXXX”表示生成 6 位随机字符。

然后,我们定义捕获错误信号:

[c.biancheng.net]$ trap "echo There exist some error!" ERR

查看已经定义的捕获:

[c.biancheng.net]$ trap

trap -- 'echo There exist some error!' ERR

此时,当我们尝试使用 rm 命令删除变量 $FILE 代表的并不存在的文件时,就会显示类似如下的错误信息:

[c.biancheng.net]$ rm $FILE

rm: cannot remove '/tmp/testtrap.8020.zafuo4': No such file or directory

There exist some error!

从上面的输出中我们看到,Shell 捕获到了文件 /tmp/testtrap.8020.zafuo4 不存在的这个错误信号,并执行了 echo 命令,显示了我们指定的错误信息。

当调试较大的脚木时,你可能想要赋予某个变量一个踪迹属性,并捕获变量的调试信息。通常,你可能只使用一个简单的赋值语句,比如,VARIABLE=value,来定义一个变量。若使用类似如下的语句替换上述的变量定义,可能会为你提供更有用的调试信息:

#声明变景 VARIABLE,并赋予其踪迹属性

declare -t VARIABLE=value

#捕获DEBUG

trap "echo VARIABLE is being used here." DEBUG

#脚本的余下部分

现在,我们创建一个名称为 testtrap1.sh 的脚本,其内容如下所示:

#!/bin/bash

#捕获退出状态0

trap 'echo "Exit 0 signal detected..."' 0

#打印信息

echo "This script is used for testing trap command."

#以状态(信号)0 退出此 Shell 脚本

exit 0

此脚本运行结果将类似如f所示:

[c.biancheng.net]$ bash ./testtrap1.sh
This script is used for testing trap command.
Exit 0 signal detected...

在上述的脚本中,trap 命令语句设置了一个当脚本以 0 状态退出时的捕获,所以当脚本以 0 状态退出时,会打印一条信息“Exit 0 signal detected...”。

我们再创建一个名称为 testtrap2.sh 的脚本,其内容类似如下所示:

#!/bin/bash

#捕获信号 SIGINT,然后打印相应信息

trap "echo 'You hit control+C! I am ignoring you.'" SIGINT

#捕获信号 SIGTERM,然后打印相应信息

trap "echo 'You tried to kill me! I am ignoring you.'" SIGTERM

#循环5次

for i in {1..5}; do

        echo "Iteration $i of 5"

        #暂停5秒

        sleep 5

done

当你运行上述脚本时,如果敲击 CTRL+C 组合键,将会中断 sleep 命令,进入下一次循环,并看到输出信息 “You hit control+C! I am ignoring you.”,但脚本 testtrap2.sh 并不会停止运行。此脚木的运行结果将类似如下所示:

[c.biancheng.net]$ bash ./testtrap2.sh
Iteration 1 of 5
You hit control+C! I am ignoring you.
Iteration 2 of 5
Iteration 3 of 5
Iteration 4 of 5
You hit control+C! I am ignoring you.
Iteration 5 of 5

当将上述脚本放在后台运行时,如果我们同时在另一个终端窗口尝试使用 kill 命令终结此脚木,此脚本并不会被终结,而是会显示信息“You tried to kill me! I am ignoring you.”, 此脚本的运行结果将会类似如下所示:

[c.biancheng.net]$ sh ./testtrap2.sh &
[1] 2320
[c.biancheng.net]$ Iteration 1 of 5
You tried to kill me! I am ignoring you.
Iteration 2 of 5
Iteration 3 of 5
Iteration 4 of 5
You tried to kill me! I am ignoring you.
Iteration 5 of 5
You tried to kill me! I am ignoring you.
[1]+ Done    sh ./testtrap2.sh

有时,接收到一个信号后你可能不想对其做任何处理。比如,当你的脚本处理较大的文件时,你可能希望阻止一些错误地输入 Ctrl+C 或 Ctrl+\ 组合键的做法,并且希望它能执行完成而不被用户中断。这时就可以使用空字符串" "' '作为 trap 的命令参数,那么 Shell 将忽略这些信号。其用法类似如下所示:

$ trap ' ' SIGHUP SIGINT [ signal... ]

十、Linux Shell trap命令捕获信号实例演示

通过前面内容的学习,我们已经知道,信号多用于以友好的方式结束一个进程的执行,即允许进程在退出之前有机会做一些清理工作。然而,信号同样还可用于其他用途。例如,当终端窗口的大小改变时,在此窗口中运行的 Shell 都会接收到信号 SIGWINCH。通常,这个信号是被忽略的,但是,如果一个程序关心窗口大小的变化,它就可以捕获这个信号,并用特定的方式处理它。

注意:除 SIGKILL 信号以外,其他任何信号都可以被捕获并通过调用C语言函数 signal 处理。

接下来,我就以一个脚本为实例演示捕获并处理 SIGWINCH 信号。我们创建名为 sigwinch_handler.sh 的脚本,其内容如下所示:

#!/bin/bash

#打印信息

echo "Adjust the size of your window now."

#捕获SIGWINCH信号

trap "echo Window size changed." SIGWINCH

#定义变量

COUNT COUNT=0

#while循环30次

while [ $C0UNT -lt 30 ]; do

        #将COUNT变量的值加1

        COUNT=$(($C0UNT + 1))

        #休眠1秒

        sleep 1

done

当上述的 Shell 脚本运行时,若改变了此脚本运行所在终端窗口的大小,脚本的进程就会收到 SIGWINCH 信号,从而调用 chwinsize 函数,以作出相应的处理。此脚本的运行结果将类似如下所示:

[c.biancheng.net]$ chmod +x sigwinch_handler.sh

[c.biancheng.net]$ ./sigwinch_handler.sh

Adjust the size of your window now.

Window size changed.

Window size changed.

我们通过上一节《十九、Linux Shell trap命令:捕获信号》的学习已经知道,在 trap 命令中可以调用函数来处理相应的信号。下面我们就以脚本 trapbg_clearup.sh 为例,来进一步学习如何使用 trap 语句调用函数来处理信号,其脚本内容如下所示:

#!/bin/bash

#捕获INT和QUIT信号,如果收到这两个信号,则执行函数 my_exit 后退出

trap 'my_exit; exit' SIGINT SIGQUIT

#捕获HUP信号

trap 'echo Going down on a SIGHUP - signal 1, no exiting...; exit' SIGHUP

#定义count变量

count=0

#创建临时文件

tmp_file=`mktemp /tmp/file.$$.XXXXXX`

#定义函数my_exit

my_exit()

{

        echo "You hit Ctrl-C/CtrI-\, now exiting..."

        #清除临时文件

        rm -f $tmp_file >& /dev/null

}

#向临时文件写入信息

echo "Do someting..." > $tmp_file

#执行无限while循环

while :

do

        #休眠1秒

        sleep 1

        #将count变量的值加1

        count=$(expr $count + 1)

        #打印count变量的值

        echo $count

done

当上述脚本运行时,接收到 SIGINT 或 SIGQUIT 信号后会调用 my_exit 函数后退出(trap 命令列表中的 exit 命令),my_exit 函数会做一些清理临时文件的操作。我们运行此脚本,然后在另一个终端窗口中查看此脚本创建的临时文件:

[c.biancheng.net]$ ls -trl /tmp/ | tail -l

将会看到类似如下的文件信息:

-rw------- 1 mozhiyan mozhiyan 15 Feb 6 22:09 file.6668.RI6669

现在,在脚本运行的终端窗口,我们输入 Ctrl+C 或 Ctrl+\ 组合键来终结或退出此脚本, 将会看到类似如下的信息:

[c.biancheng.net]$ ./trapbg_clearup.sh

1

2

3

4

5

6

7

8

9

You hit Ctrl+C/Ctrl+\, now exiting...

然后我们再查看一下脚本创建的临时文件是否巳被清理:

[c.biancheng.net]$ ls -l /tmp/file.6668.RI6669

ls: /tmp/file.6668.RI6669: No such file or directory

当脚本运行在后台时,同样可以捕获信号。我们将上例中的脚本 trapbg_clearup.sh 放在后台运行:

[c.biancheng.net]$ ./trapbg_clearup.sh &

[1] 16957

[c.biancheng.net]$ 1

2

3

现在从另一个终端窗口,发送 HUP 信号来杀掉这个运行脚本的进程:

[c.biancheng.net]$ kill -l 16957

现在,在脚本运行的终端窗口,将看到类似如下的信息:

[c.biancheng.net]$ ./trapbg_clearup.sh &

[1] 16957

[c.biancheng.net]$ 1

2

3

4

5

6

7

8

9

10

 Going down on a SIGHUP - signal 1, now exiting...

[1]+ Done ./trapbg_clearup.sh

LINENO 和 BASH_COMMAND 变量

Bash Shell 中有两个内部变量可以方便地在处理信号时,为我们提供更多的与脚本终结相关的信息。这两个变埴分别是 LINENO 和 BASH_COMMAND。BASH_COMMAND是 Bash 中特有的。这两个变量分别用于报告脚本当前执行的行号和脚本当前运行的命令。

下面,我们以脚本 trap_report.sh 为实例,学习如何在脚本中使用变量 LINENO 和 BASH_COMMAND 在脚本终结时为我们提供更多的错误信息,其脚本内容类似如下所示:

#!/bin/bash

#捕获SIGHUP、SIGINT和SIGQUIT信号。如果收到这些信号,将执行函数my_exit后退出

trap 'my_exit $LINENO $BASH_COMMAND; exit' SIGHUP SIGINT SIGQUIT

#函数my_exit

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"

        #其他一些清埋命令

}

#执行无限while循环

while :

do

        #休眠1秒

        sleep 1

        #将变量count的值加1

        count=$(expr $count + 1)

        #打印count变量的值

        echo $count

done

当上述脚本运行时,向脚本发送 SIGHUP、SIGINT 和 SIGQUIT 信号后,脚本将会调用 my_exit 函数,此函数将解析参数 $l(LINENO) 和 $2(BASH_COMMAND),显示信号被捕获时脚本所运行的命令及其行号,同样 logger 语句会记录信息到日志文件 /var/log/messages 中。如果需要,还可以在此函数中执行一些清理命令,然后脚本将会退出(trap 命令列表中的 exit 命令)。

此脚木的运行结果将会类似如下所示:

[c.biancheng.net]$ ./trap_report.sh

1

2

3

4

5

trap_report.sh caught error on line : 34 command was: sleep

在 /var/log/messages 文件中,将会看到一条类似如下的记录:

Feb 7 16:48:13 localhost mozhiyan: script: trap_report.sh was terminated: line: 34, command was sleep

我们在上一节《十九、Linux Shell trap命令:捕获信号》中已经学习了,使用 trap 语句可以忽略信号。你也同样可以在脚本的—部分中忽略某些信号,然后,当你希望捕获这些信号时,可以重新定义它们来采取一些行动。我们以脚本 trapoff_on.sh 为例,在此脚本中我们将忽略信号 SIGINT 和 SIGQUIT,直到 sleep 命令结束运行后为止。然后当下一个 sleep 命令开始时,如果接收到终结信号,trap 语句将采取相应的行动。

其脚木的内容如下所示:

#!/bin/bash

#忽略SIGINT和SIGQUIT信号

trap ' '  SIGINT SIGQUIT

#打印提示信息

echo "You cannot terminate using ctrl+c or ctrl+\!"

#休眠10秒

sleep 10

#重新捕获SIGINT和SIGQUIT信号。如果捕获到这两个信号,则打印信息后退出

#现在可以中断脚本了

trap 'echo Terminated!; exit' SIGINT SIGQUIT

#打印提示信息

echo "OK! You can now terminate me using those keystrokes"

#休眠10秒

sleep 10

此脚本的运行结果将类似如下所示:

[c.biancheng.net]$ chmod +x trapoff_on.sh

[c.biancheng.net]$ ./trapoff_on.sh

You cannot terminate using ctrl+c or ctrl+\!

OK! You can now terminate me using those keystrokes.

Terminated!

二十一、Linux Shell移除(重置)信号捕获

如果我们在脚本中应用了捕获,我们通常会在脚本的结尾处,将接收到信号时的行为处理重置为默认模式。重置(移除)捕获的语法如下所示:

$ trap - signal [ signal ... ]

从上述语法中可以看出,使用破折号作为 trap 语句的命令参数,就可以移除信号的捕获。

下面,我们以脚本 trap_reset.sh为例,来学习如何在脚本中移除先前定义的捕获。其脚本的内容类似如下所示:

#!/bin/bash

#定义函数cleanup

function cleanup {

        #如果变量 msgfile 所指定的文件存在

        if [[ -e $msgfile ]]; then

        #将文件重命名(或移除)

        mv $msgfile $msgfile.dead

        fi

        exit

}

#捕获INT和TERM信号

trap cleanup INT TERM

#创建一个临时文件

msgfile=`mktemp /tmp/testtrap.$$.XXXXXX`

#通过命令行向此临时文件中写入内容

cat > $msgfile

#接下来,发送临时文件的内容到指定的邮件地址,你自己完善此部分代码

#send the contents of $msgfile to the specified mail address...

#删除临时文件

rm $msgfile

#移除信号INT和TERM的捕获

trap - INT TERM

上述脚本中,在用户已经完成了发送邮件的操作之后,临时文件会被删除。这时,因为已经不再需要清理操作,我们可以重置信号的捕获到默认状态,所以我们在脚本的最后一行重置了 INT 和 TERM 信号的捕获。

二十二、关于Linux Shell中进程、信号和捕获的总结

下面我们总结一下前面几节学到的关于进程、信号和捕获的主要知识。

在 Linux 系统和其他类 Unix 或 Unix 操作系统中,信号被用于进程间的通信。

信号是一个发送到某个进程或同一进程中的特定线程的异步通知,用于通知发生的一个事件。

在 Linux 中,信号在处理异常和中断方面,扮演了极其重要的角色。

当一个事件发生时,会产生一个信号,然后内核会将事件传递到接收的进程。

运行在用户模式下的进程会接收信号。如果接收的进程正运行在内核模式,那么信号的执行只有在该进程返回到用户模式时才会开始。

当进程收到一个信号时,可能会发生以下3种情况:

  • 进程可能会忽略此信号。有些信号不能被忽略,而有些没有默认行为的信号,默认会被忽略。
  • 进程可能会捕获此信号,并执行一个被称为信号处理器的特殊函数。
  • 进程可能会执行信号的默认行为。例如,信号 15(SIGTERM) 的默认行为是结束进程。

在Shell命令行提示符下,输入“kill -l”命令,可以显示所有信号的信号值和相应的信号名。

由 Bash Shell 运行的非内部命令会使用 Shell 从其父进程继承的信号处理程序。

默认情况下,Shell 接收到 SIGHUP 信号后会退出。在退出之前,一个交瓦式的 Shell 会向所有的作业,不管是正在运行的还是已停止的,重新发送 SIGHUP 信号。

若要阻止 Shell 向某个特定的作业发送 SIGHUP 信号,可以使用内部命令 disown 将它从作业表中移除,或是用“disown -h”命令阻止 Shell 向特定的作业发送 SIGHUP 信号,但并不会将特定的作业从作业表中移除。

进程是运行在 Linux 中的程序的一个实例。

每当你在 Linux 中执行一个命令,它都会创建或启动一个新的进程。

有两种运行方式的进程:前台进程和后台进程。

进程可以有5种状态:不可中断休眠状态(D)、运行状态(R)、休眠状态(S)、 停止状态(T)和僵死状态(Z)。

使用 ps 命令,可以查看当前的进程;使用 pstree 命令,可以显示进程树的信息;使用 pgrep 命令,可以基于名称或其他属性查找进程。

当准备杀掉一个进程或一连串的进程时,我们的常识是从尝试发送最安全的信号开始,即 SIGTERM 信号。

如果发送一个 SIGKILL 信号到进程,将消除进程先清理而后关闭的机会,这可能导致不幸的结果。但如果一个有序地终结不管用,那么发送 SIGINT 或 SIGKILL 信号就可能是唯一的方法了。

killall 命令会发送信号到运行任何指定命令的所有进程。

使用 pkill 命令,可以通过指定进程名、用户名、组名、终端、UID、EUID 和 GID 等属性来杀掉相应的进程。pkill 命令默认也是发送 SIGTERM 信号到进程。

Bash 的内部命令 trap,让我们可以在 Shell 脚本内捕获特定的信号并对它们进行处理。

使用空字符串""''作为 trap 的命令参数,可以让 Shell 忽略指定的信号。

除 SIGKILL 信号以外,其他任何信号都可以被捕获并通过调用C语言函数 signal 处理。

Bash中有两个内部变量 LINENO 和 BASH_COMMAND 可以方便地在处理信号时,分别用于报告脚本当前执行的行号和脚本当前运行的命令。

使用破折号作为 trap 语句的命令参数,就可以移除指定信号的捕获。

二十三、Shell模块化(把代码分散到多个脚本文件中)

所谓模块化,就是把代码分散到多个文件或者文件夹。对于大中型项目,模块化是必须的,否则会在一个文件中堆积成千上万行代码,这简直是一种灾难。

基本上所有的编程语言都支持模块化,以达到代码复用的效果,比如,Java 和 Python 中有 import,C/C++ 中有 #include。在 Shell 中,我们可以使用 source 命令来实现类似的效果。

《Shell基础 十一:执行Shell脚本》一节中我们已经提到了 source 命令,这里我们再来讲解一下。

source 命令的用法为:

source filename

也可以简写为:

. filename

两种写法的效果相同。对于第二种写法,注意点号.和文件名中间有一个空格。

source 是Shell内置命令的一种,它会读取 filename 文件中的代码,并依次执行所有语句。你也可以理解为,source 命令会强制执行脚本文件中的全部命令,而忽略脚本文件的权限。

1、实例

创建两个脚本文件 func.sh 和 main.sh:func.sh 中包含了若干函数,main.sh 是主文件,main.sh 中会包含 func.sh。

func.sh 文件内容:

 #计算所有参数的和

 function sum(){

         local total=0

         for n in $@

         do

                 ((total+=n))

         done

         echo $total

         return 0

 }

main.sh 文件内容:

 #!/bin/bash

 source func.sh

echo $(sum 10 20 55 15)

运行 main.sh,输出结果为:
100

source 后边可以使用相对路径,也可以使用绝对路径,这里我们使用的是相对路径。

2、避免重复引入

熟悉 C/C++ 的读者都知道,C/C++ 中的头文件可以避免被重复引入;换句话说,即使被多次引入,效果也相当于一次引入。这并不是 #include 的功劳,而是我们在头文件中进行了特殊处理。

Shell source 命令和 C/C++ 中的 #include 类似,都没有避免重复引入的功能,只要你使用一次 source,它就引入一次脚本文件中的代码。

那么,在 Shell 中究竟该如何避免重复引入呢?

我们可以在模块中额外设置一个变量,使用 if 语句来检测这个变量是否存在,如果发现这个变量存在,就 return 出去。

这里需要强调一下 return 关键字。return 在 C++、C#、Java 等大部分编程语言中只能退出函数,除此以外再无他用;但是在 Shell 中,return 除了可以退出函数,还能退出由 source 命令引入的脚本文件。

所谓退出脚本文件,就是在被 source 引入的脚本文件(子文件)中,一旦遇到 return 关键字,后面的代码都不会再执行了,而是回到父脚本文件中继续执行 source 命令后面的代码。

return 只能退出由 source 命令引入的脚本文件,对其它引入脚本的方式无效。

下面我们通过一个实例来演示如何避免脚本文件被重复引入。本例会涉及到两个脚本文件,分别是主文件 main.sh 和 模块文件 module.sh。

模块文件 module.sh:

if [ -n "$__MODULE_SH__" ]; then

        return

fi

__MODULE_SH__='module.sh'

echo "http://c.biancheng.net/shell/"

注意第一行代码,一定要是使用双引号把$__MODULE_SH__包围起来,具体原因已经在《Shell编程 三十一:Shell test命令(Shell [])详解,附带所有选项及说明》一节中讲到。

主文件 main.sh:

#!/bin/bash

source module.sh

source module.sh

echo "here executed"

./表示当前文件,你也可以直接写作source module.sh

运行 main.sh,输出结果为:

http://c.biancheng.net/shell/
here executed

我们在 main.sh 中两次引入 module.sh,但是只执行了一次,说明第二次引入是无效的。

main.sh 中的最后一条 echo 语句产生了输出结果,说明 return 只是退出了子文件,对父文件没有影响。

十二月5日上午发

你可能感兴趣的:(Linux,chrome,前端)