Skip to content

控制脚本

处理信号

Linux利用信号在系统进程之间进行通讯, 用这些信号来停止、启动、终止进程。可以通过对脚本进行编程, 使其在收到特定信号时执行某些命令,从而控制shell脚本的操作

默认情况下,bash shell会忽略收到的任何SIGQUIT (3)和SIGTERM (5)信号(正因为这样,交互式shell才不会被意外终止)。但是bash shell会处理收到的SIGHUP (1)和SIGINT (2)信号

如果bash shell收到了SIGHUP信号,比如当你要离开一个交互式shell,它就会退出。但在退出之前,它会将SIGHUP信号传给所有由该shell所启动的进程

通过SIGINT信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这种情况发生时,shell会将SIGINT信号传给所有由它所启动的进程,以此告知出现的状况

shell会将这些信号传给shell脚本程序来处理。而shell脚本的默认行为是忽略这些信号。它们可能会不利于脚本的运行。要避免这种情况,你可以脚本中加入识别信号的代码,并执行命令来处理信号

生成信号

中断进程

Ctrl+C发送SIGINT信号, 停止shell当前的进程

暂停进程

在进程运行期间暂停

尽管有时这可能会比较危险(比如,脚本打开了一个关键的系统文件的文件锁),但通常它可以在不终止进程的情况下使你能够深入脚本内部一窥究竟

Ctrl+Z组合键会生成一个SIGTSTP信号,停止shell中运行的任何进程

停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行

既然你已经知道了已停止作业的PID,就可以用kill命令来发送一个SIGKILL信号来终止它

但下次如果你做了能够产生shell提示符的操作(比如按回车键),你就会看到一条消息,显示作业已经被终止了。每当shell产生一个提示符时,它就会显示shell中状态发生改变的作业的状态。在你终止一个作业后,下次强制shell生成一个提示符时,shell会显示一条消息,说明作业在运行时被终止了

捕获信号

trap命令允许你来指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再由shell处理,而是交由本地处理

bash
trap commnd signals

你只要列出想要shell执行的命令,以及一组用空格分开的待捕获的信号。你可以用数值或Linux信号名来指定信号

bash
  1 #!/bin/bash                                                                           
  2 
  3 trap "echo 'Sorry! I have trapped Ctrl+C'" SIGINT
  4 echo This is a test script
  5 
  6 count=1
  7 while [ $count -le 10 ]
  8 do
  9     echo "Loop #$count"
 10     sleep 1
 11     count=$[ $count + 1 ]
 12 done
 13 
 14 echo "This is the end of the test script"

捕获脚本退出

在shell脚本退出的时候进行捕获, 在shell完成任务时候执行命令的简便方法

bash
  1 #!/bin/bash                                                                           
  2 
  3 trap "echo Goodbye..." EXIT
  4 
  5 count=1
  6 while [ $count -le 5 ]
  7 do
  8     echo "Loop #$count"
  9     sleep 1
 10     count=$[ $count +1 ]
 11 done

在退出之前执行的程序

修改或移除捕获

要想在脚本中的不同位置进行不同的捕获处理,只需重新使用带有新选项的trap命令

bash
  1 #!/bin/bash                                                                           
  2 
  3 trap "echo 'Sorry... Ctrl-C is traped...'"  SIGINT
  4 count=1
  5 while [ $count -le 5 ]
  6 do
  7     echo "Loop #$count"
  8     sleep 1
  9     count=$[ $count + 1 ]
 10 done
 11 trap "echo 'I modified the trap!'" SIGINT
 12 count=1
 13 while [ $count -le 5 ]
 14 do
 15     echo "Second Loop #$count"
 16     sleep 1
 17     count=$[ $count + 1 ]
 18 done

也可以删除已设置好的捕获。只需要在trap命令与希望恢复默认行为的信号列表之间加上两个破折号就行了

bash
  1 #!/bin/bash                                                                           
  2 
  3 trap "echo 'Sorry ...Ctrl + C is trapped.'" SIGINT
  4 count=1
  5 while [ $count -le 5 ]
  6 do
  7     echo "Loop #count "
  8     sleep 1
  9     count=$[ $count + 1 ]
 10 done
 11 
 12 trap -- SIGINT
 13 echo "I just removed the trap"
 14 count=1
 15 while [ $count -le 5 ] 
 16 do
 17     echo "Second Loop #$count"
 18     sleep 1
 19     count=$[ $count + 1 ]
 20 done

也可以在trap命令后使用单破折号来恢复信号的默认行为。单破折号和双破折号都可以正常发挥作用。

在后台运行脚本

有的脚本运行在shell之中不怎么方便, 有的脚本运行需要很长时间

在后台运行的时候不会和终端会话上的STDIN、STDOUT以及STDERR关联

后台运行脚本

当&符放到命令后时,它会将命令和bash shell分离开来,将命令作为系统中的一个独立的后台进程运行。显示的第一行是

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ test5.sh &
[1] 3419
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ 
[1]+  已完成               test5.sh

当后台进程运行时,它仍然会使用终端显示器来显示STDOUT和STDERR消息

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ test6.sh &
[1] 3464
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ Start the test scrip
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
test scrip is complete

最好是将后台运行的脚本的STDOUT和STDERR进行重定向,避免这种杂乱的输出

运行多个后台作业

注意,在ps命令的输出中,每一个后台进程都和终端会话(pts/0)终端联系在一起。如果终端会话退出,那么后台进程也会随之退出

在非控制台下运行脚本

在终端会话中启动shell脚本,然后让脚本一直以后台模式运行到结束,即使你退出了终端会话。这可以用nohup命令来实现

nohup命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP信号。这会在退出终端会话时阻止进程退出

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ nohup test6.sh &
[1] 3484
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ nohup: 忽略输入并把输出追加到'nohup.out'

jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ ls
nohup.out  teset1  test1.sh  test2.sh  test3.sh  test4.sh  test5.sh  test6.sh
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ cat nohup.out 
Start the test scrip
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
test scrip is complete
[1]+  已完成               nohup test6.sh

会把输出重定向到nohup.out文件之中

作业控制

暂停的进程可以选择是停止还是继续运行, 可以使用kill命令终止进程, 重新运行, 需要发送一个SIGCONT信号

启动、停止、终止以及恢复作业的这些功能统称为作业控制

查看作业

关键命令jobs命令, 允许查看当前运行的作业的状态

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ jobs
[1]-  已停止               test7.sh
[2]+  已停止               test7.sh > test7out
[3]   运行中               test7.sh > test7out &

jobs命令输出中的加号和减号。带加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号

带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业

默认进程(带有加号的那个)是最后启动的那个进程

参数描述
-l(小写L)列出进程的PID以及作业号
-n只列出上次shell发出的通知后改变了状态的作业
-p只列出作业的PID
-r只列出运行中的作业
-s只列出已停止的作业

重启停止的作业

可以将已停止的作业作为后台进程或前台进程重启。前台进程会接管你当前工作的终端

要以后台模式重启一个作业,可用bg命令加上作业号

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ jobs
[1]-  已停止               test7.sh
[2]+  已停止               test7.sh > test7out
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ bg
[2]+ test7.sh > test7out &
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ jobs
[1]+  已停止               test7.sh
[2]-  运行中               test7.sh > test7out &

默认任务不需要加任务号

可以使用bg加上任务号进行启动bg 2

以前台模式启动一个作业fg

bash
$ fg 2
./test12.sh 
This is the script's end...

调整谦让度

CPU分配给每一个任务的时间

调度优先级是整数, 从-20(最高优先级)到+19(最低优先级)

bash shell默认会以0启动所有进程

nice命令

让命令以更低的优先级运行, -n 命令

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ nice -n 10 test7.sh  > test7out &
[1] 3957
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ ps -p 3957 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
   3957    2840  10 /bin/bash ./test7.sh
  • 命令会禁止普通用户提高等级
nash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ nice -n -10 test7.sh  > test7out &
[2] 4044
[1]   已完成               nice -n 10 test7.sh > test7out
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ nice: 无法设置优先级: 权限不够
  • nice命令的-n选项并不是必须的,只需要在破折号后面跟上优先级就行了
bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ nice -10 test7.sh  > test7out &
[3] 4083
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ ps -p 4083 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
   4083    2840  10 /bin/bash ./test7.sh

renice命令

改变正在运行的程序的优先级, 它允许你指定运行进程的PID来改变它的优先级

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ test7.sh  > test7out &
[5] 4188
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ ps -p 4188 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
   4188    2840   0 /bin/bash ./test7.sh
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ renice -n 10 -p 4188
4188 (process ID) 旧优先级为 0,新优先级为 10
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ ps -p 4188 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
   4188    2840  10 /bin/bash ./test7.sh
  • 只能对属于你的进程执行renice;
  • 只能通过renice降低进程的优先级;
  • root用户可以通过renice来任意调整进程的优先级

定时运行作业

at命令和cron表。每个方法都使用不同的技术来安排脚本的运行时间和频率。接下来会依次介绍这些方法

使用at命令执行作业

允许指定Linux何时运行命令, 把作业提交到队列中, 指定shell何时运行作业

at的守护进程atd会以后台模式运行,检查作业队列来运行作业

atd守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at)来获取用at命令提交的作业。默认情况下,atd守护进程会每60秒检查一下这个目录, 如果时间跟当前时间匹配,atd守护进程就会运行此作业

at命令格式

bash
at [-f filename] time

at命令会将STDIN的输入放到队列中。你可以用-f参数来指定用于读取命令(脚本文件)的文件名

time参数指定了Linux系统何时运行该作业。如果你指定的时间已经错过,at命令会在第二天的那个时间运行指定的作业

识别的格式

  • 标准的小时和分钟格式,比如10:15
  • AM/PM指示符,比如10:15 PM
  • 特定可命名时间,比如now、noon、midnight或者teatime(4 PM)

除了指定运行作业的时间,也可以通过不同的日期格式指定特定的日期

  • 标准日期格式,比如MMDDYY、MM/DD/YY或DD.MM.YY。

  • 文本日期比如Jul 4或Dec 25,加不加年份均可

  • 指定时间增量

    明天10:15 PM

    10:15+7天

    当前时间+25 min

该作业会被提交到作业队列

作业队列会保存通过at命令提交的待处理的作业。针对不同优先级,存在26种不同的作业队列。作业队列通常用小写字母a~z和大写字母A~Z来指代

在几年前,也可以使用batch命令在指定时间执行某个脚本。batch命令很特别,你可以安排脚本在系统处于低负载时运行。但现在batch命令只不过是一个脚本而已(/usr/bin/batch),它会调用at命令并将作业提交到b队列中

作业队列的字母排序越高,作业运行的优先级就越低(更高的nice值)

一般情况作业会被提交到a作业队列。如果想以更高优先级运行作业,可以用-q参数指定不同的队列字母

获取作业输出

Linux系统会将提交该作业的用户的电子邮件地址作为STDOUT和STDERR。任何发到STDOUT或STDERR的输出都会通过邮件系统发送给该用户

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ at -f test8.sh now
warning: commands will be executed using /bin/sh
job 1 at Mon Aug  8 14:06:00 2022

使用e-mail作为at命令的输出极其不便。at命令利用sendmail应用程序来发送邮件。如果你的系统中没有安装sendmail,那就无法获得任何输出

最好在脚本中对STDOUT和STDERR进行重定向

bash
  1 #!/bin/bash                                                                           
  2 
  3 echo "This script run at $(date +%B%d,%T)" > test9out
  4 echo >> test9out
  5 sleep 5
  6 echo "This is the script's end...'" >> test9out
  
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ at -f test9.sh now
warning: commands will be executed using /bin/sh
job 3 at Mon Aug  8 14:15:00 2022
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ cat test9out
This script run at 八月08,14:15:50

This is the script's end...'
  • 如果不想在at命令中使用邮件或重定向,最好加上-M选项来屏蔽作业产生的输出信息

列出等待的作业

atq命令

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ at -M -f test9.sh teatime
warning: commands will be executed using /bin/sh
job 4 at Mon Aug  8 16:00:00 2022
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ at -M -f test9.sh tommorow
syntax error. Last token seen: t
Garbled time
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ at -M -f test9.sh tomorrow
warning: commands will be executed using /bin/sh
job 5 at Tue Aug  9 14:22:00 2022
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ at -M -f test9.sh 13:30
warning: commands will be executed using /bin/sh
job 6 at Tue Aug  9 13:30:00 2022
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ atq
4	Mon Aug  8 16:00:00 2022 a jiao
5	Tue Aug  9 14:22:00 2022 a jiao
6	Tue Aug  9 13:30:00 2022 a jiao

删除作业

bash
atrm 编号
bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ atq
4	Mon Aug  8 16:00:00 2022 a jiao
5	Tue Aug  9 14:22:00 2022 a jiao
6	Tue Aug  9 13:30:00 2022 a jiao
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ atrm 4
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ atrm 5
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ atq
6	Tue Aug  9 13:30:00 2022 a jiao

安排需要定期执行的脚本

Linux系统使用cron程序来安排要定期执行的作业。cron程序会在后台运行并检查一个特殊的表(被称作cron时间表),以获知已安排执行的作业

cron时间表

cron时间表采用一种特别的格式来指定作业何时运行。其格式如下:

bash
min hour dayofmonth month dayofweek command

cron时间表允许你用特定值、取值范围(比如1~5)或者是通配符(星号)来指定条目

bash
15 10 * * * command

可以用三字符的文本值(mon、tue、wed、thu、fri、sat、sun)或数值(0为周日,6为周六)来指定dayofweek表项

如何设置一个在每个月的最后一天执行的命令,因为你无法设置dayofmonth的值来涵盖所有的月份。这个问题困扰着Linux和Unix程序员,也激发了不少解决办法。常用的方法是加一条使用date命令的if-then语句来检查明天的日期是不是01:00 12 * * * if [date +%d -d tomorrow = 01 ] ; then ; command它会在每天中午12点来检查是不是当月的最后一天,如果是,cron将会运行该命令

命令列表必须指定要运行的命令或脚本的全路径名。你可以像在普通的命令行中那样,添加任何想要的命令行参数和重定向符号

会用提交作业的用户账户运行该脚本。因此,你必须有访问该命令和命令中指定的输出文件的权限

创建crno表

crontab命令来处理cron时间表。要列出已有的cron时间表,可以用-l选项。

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ crontab -l
no crontab for jiao

默认情况下,用户的cron时间表文件并不存在。要为cron时间表添加条目,可以用-e选项。在添加条目时,crontab命令会启用一个文本编辑器(参见第10章),使用已有的cron时间表作为文件内容(或者是一个空文件,如果时间表不存在的话)。

浏览cron目录

如果你创建的脚本对精确的执行时间要求不高,用预配置的cron脚本目录会更方便。有4个基本目录:hourly、daily、monthly和weekly

bash'
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ ls /etc/cron.*ly
/etc/cron.daily:
0anacron  apport  apt-compat  aptitude  cracklib-runtime  dpkg  logrotate  man-db

/etc/cron.hourly:

/etc/cron.monthly:
0anacron

/etc/cron.weekly:
0anacron  man-db

脚本每天都要运行就把他复制到daily目录

anacron程序

唯一的问题是, 它的假设是Linux二十四小时运行

如果某个作业在cron时间表中安排运行的时间已到,但这时候Linux系统处于关机状态,那么这个作业就不会被运行。当系统开机时,cron程序不会再去运行那些错过的作业

要解决这个问题,许多Linux发行版还包含了anacron程序。如果anacron知道某个作业错过了执行时间,它会尽快运行该作业。这意味着如果Linux系统关机了几天,当它再次开机时,原定在关机期间运行的作业会自动运行

anacron程序只会处理位于cron目录的程序,比如/etc/cron.monthly。它用时间戳来决定作业是否在正确的计划间隔内运行了。每个cron目录都有个时间戳文件,该文件位于/var/spool/ anacron。

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ sudo cat /var/spool/anacron/cron.monthly [sudo] jiao 的密码: 
20220719

anacron程序使用自己的时间表(通常位于/etc/anacrontab)来检查作业目录

bash
jiao@jiao-virtual-machine:~/桌面/linux-shell/16$ sudo cat /etc/anacrontab 
# /etc/anacrontab: configuration file for anacron

# See anacron(8) and anacrontab(5) for details.

SHELL=/bin/sh
HOME=/root
LOGNAME=root

# These replace cron's entries
1	5	cron.daily	run-parts --report /etc/cron.daily
7	10	cron.weekly	run-parts --report /etc/cron.weekly
@monthly	15	cron.monthly	run-parts --report /etc/cron.monthly

anacron时间表的基本格式和cron时间表略有不同:

bash
period delay identifier command

period条目定义了作业多久运行一次,以天为单位。anacron程序用此条目来检查作业的时间戳文件。delay条目会指定系统启动后anacron程序需要等待多少分钟再开始运行错过的脚本。command条目包含了run-parts程序和一个cron脚本目录名。run-parts程序负责运行目录中传给它的任何脚本

anacron不会运行位于/etc/cron.hourly的脚本。这是因为anacron程序不会处理执行时间需求小于一天的脚本

使用新的shell启动脚本

基本上,依照下列顺序所找到的第一个文件会被运行,其余的文件会被忽略:

  • $HOME/.bash_profile
  • $HOME/.bash_login
  • $HOME/.profile

因此,应该将需要在登录时运行的脚本放在上面第一个文件中

每次启动一个新shell时,bash shell都会运行.bashrc文件。

.bashrc文件通常也是通过某个bash启动文件来运行的。因为.bashrc文件会运行两次:一次是当你登入bash shell时,另一次是当你启动一个bash shell时。如果你需要一个脚本在两个时刻都得以运行,可以把这个脚本放进该文件中