Linux命令行与Shell脚本编程
第十五章 脚本控制
文章目录
五,脚本控制
1,处理信号
Linux利用信号与系统中的进程进行通信。
1.1,Linux信号
Linux系统和应用程序可以产生超过30个信号。
常用信号:
信号 | 值 | 描述 |
---|---|---|
1 | SIGHUP | 挂起(hangup)进程 |
2 | SIGINT | 中断(interrupt)进程 |
3 | SIGQUIT | 停止(stop)进程 |
9 | SIGKILL | 无条件终止(terminate)进程 |
15 | SIGTERM | 尽可能终止进程 |
18 | SIGCONT | 继续运行停止的进程 |
19 | SIGSTOP | 无条件停止,但不终止进程 |
20 | SIGTSTP | 停止或暂停(pause),但不终止进程 |
默认情况,bashShell会忽略收到的任何 SIGQUIT(3) 信号和 SIGTERM(15) 信号,bash shell会处理收到的所有SIGHUP(1)信号和SIGINT(2)信号。
1.2,产生信号
1.2.1,中断进程 SIGINT 2
Ctrl+C组合键会生成 SIGINT信号,并将其发送给当前在 shell中运行的所有进程。
$ sleep 60
^C
$
1.2.2,暂停进程 SIGTSTP 20
Ctrl+Z组合键会生成 SIGTSTP 信号,停止shell中运行的任何进程。停止进程跟终止进程不同,前者让程序继续驻留在内存中,还能从上次停止的位置继续运行。
使用Ctrl+Z组合键时,shell会通知进程停止
$ sleep 60
^Z
[2]+ Stopped sleep 60
[n]中的数字是 shell分配的作业号。 shell将运行的各个进程称为作业,并为作业在当前 shell内分配了唯一的作业号。
作业号从1开始,依次递增。
如果shell会话中有一个已停止的作业,在退出shell时,bash会发出提醒:
$ sleep 70
^Z
[1]+ Stopped sleep 70
$ exit
logout
There are stopped jobs.
用ps命令查看已停止的作业,在 S列(进程状态)中,ps命令将已停止作业的状态显示为T。这说明命令要么被跟踪,要么被停止。
$ ps -l
F S UID PID PPID [...] TTY TIME CMD
0 S 1001 1509 1508 [...] pts/0 00:00:00 bash
0 T 1001 1532 1509 [...] pts/0 00:00:00 sleep
0 T 1001 1533 1509 [...] pts/0 00:00:00 sleep
0 R 1001 1534 1509 [...] pts/0 00:00:00 ps
如果 在有已停止作业的情况下仍旧想退出shell,则只需再输入一遍exit命令,shell会退出,终止已停止作业。
如果知道已停止作业的PID,那就可以用kill命令发送 SIGKILL(9) 信号将其终止:
$ kill -9 1532
[1]- Killed sleep 60
$ kill -9 1533
[2]+ Killed sleep 70
1.3,捕获信号 trap commands signals
可以用其他命令在信号出现时将其捕获,而不是忽略信号。
trap命令 可以指定shell脚本需要侦测并拦截的Linux信号。如果脚本收到了 trap命令中列出的信号,则该信号不再由shell处理,而是由本地处理。
格式:
trap commands signals
commands 列出想要shell执行的命令,signals 列出想要捕获的信号(多个信号之间以空格分隔)。指定信号的时候,可以使用信号的值或信号名。
每次使用Ctrl+C组合键,脚本都会执行trap命令中指定的echo语句,而不是忽略信号并让shell停止该脚本。
$ cat trapsignal.sh
#!/bin/bash
trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT
echo This is a test script.
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
echo "This is the end of test script."
exit
$ ./trapsignal.sh
This is a test script.
Loop #1
Loop #2
^C Sorry! I have trapped Ctrl-C
Loop #3
^C Sorry! I have trapped Ctrl-C
Loop #4
Loop #5
This is the end of test script.
如果脚本中的命令被信号中断,使用带有指定命令的trap未必能让被中断的命令继续执行。
为了保证脚本中的关键操作不被打断,使用带有空操作命令的 trap以及要捕获的信号列表:
trap "" SIGINT
这种形式的 trap命令允许脚本完全忽略SIGINT信号,继续执行重要的工作.
1.4,捕获脚本退出 trap EXIT
除了在shell脚本中捕获信号,也可以在shell脚本退出时捕获信号。在shell完成任务时执行命令的一种简便方法。
捕获shell脚本的退出,只需在trap命令后加上EXIT信号即可
$ cat trapexit.sh
#!/bin/bash
trap "echo Goodbye..." EXIT
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
exit
$ ./trapexit.sh
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Goodbye...
$ ./trapexit.sh ## 如果提前退出脚本,则依然能捕获到EXIT:
Loop #1
Loop #2
Loop #3
^CGoodbye...
1.5,修改或移除信号捕获
在脚本中的不同位置进行不同的信号捕获处理,重新使用带有新选项的trap命令:
$ cat trapmod.sh
#!/bin/bash
trap "echo ' Sorry...Ctrl-C is trapped.'" SIGINT
count=1
while [ $count -le 3 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
trap "echo ' I have modified the trap!'" SIGINT
count=1
while [ $count -le 3 ]
do
echo "Second Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
exit
$ ./trapmod.sh
Loop #1
^C Sorry...Ctrl-C is trapped.
Loop #2
Loop #3
Second Loop #1
Second Loop #2
^C I have modified the trap!
Second Loop #3
在交互式shell会话中使用trap命令,可以使用 trap -p查看被捕获的信号。如果什么都没有显示,则说明shell会话按照默认方式处理信号。
移除已设置好的信号捕获。在trap命令与希望恢复默认行为的信号列表之间加上连字符(单连字符和双连字符的效果一样):
$ cat trapremoval.sh
#!/bin/bash
trap "echo ' Sorry...Ctrl-C is trapped.'" SIGINT
count=1
while [ $count -le 3 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
trap -- SIGINT ## 移除trap
echo "The trap is now removed."
count=1
while [ $count -le 3 ]
do
echo "Second Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
exit
$ ./trapremoval.sh
Loop #1
Loop #2
^C Sorry...Ctrl-C is trapped.
Loop #3
The trap is now removed.
Second Loop #1
Second Loop #2
^C
2,以后台模式运行脚本
有些脚本可能要执行很长一段时间,而不想在命令行界面等待.在后台模式中,进程运行时不和终端会话的 STDIN、STDOUT以及STDERR关联.
可以用shell脚本试试这个特性,允许脚本在后台运行,不占用终端会话。
2.1,后台运行脚本
以后台模式运行shell脚本非常简单,只需在脚本名后面加上&:
$ cat backgroundscript.sh
#!/bin/bash
count=1
while [ $count -le 5 ]
do
sleep 1
count=$[ $count + 1 ]
done
exit
$ ./backgroundscript.sh &
[1] 2595
[1]是shell分配给后台进程的作业号,之后的数字(2595)是Linux系统为进程分配的进程ID(PID).
当后台进程结束时,终端上会显示一条消息:
[1]+ Done ./backgroundscript.sh
当后台进程运行时,仍然会使用终端显示器来显示STDOUT和STDERR消息!!!
最好是将后台脚本的STDOUT和STDERR进行重定向,避免这种杂乱的输出。
2.2,运行多个后台作业
使用命令行提示符的情况下,可以同时启动多个后台作业:
Linux系统都会为其分配新的作业号和PID。通过ps命令可以看到,所有脚本都处于运行状态:
$ ps
PID TTY TIME CMD
1509 pts/0 00:00:00 bash
2753 pts/0 00:00:00 testAscript.sh
2754 pts/0 00:00:00 sleep
2755 pts/0 00:00:00 testBscript.sh
2756 pts/0 00:00:00 sleep
2757 pts/0 00:00:00 testCscript.sh
2758 pts/0 00:00:00 sleep
2759 pts/0 00:00:00 testDscript.sh
2760 pts/0 00:00:00 sleep
2761 pts/0 00:00:00 ps
$
每一个后台进程都和终端会话(pts/0)终端关联在一起。如果终端会话退出,那么后台进程也会随之退出!!!!!
当要退出终端会话时,如果还有被停止的进程,就会出现警告信息。但如果是后台进程,则只有部分终端仿真器会在退出终端会话前提醒你尚有后台作业在运行。
如果在登出控制台后,仍希望运行在后台模式的脚本继续运行,则需要借助其他手段。
3,在非控制台下运行脚本 nohup
有时候退出了终端会话,也需要在终端会话中启动shell脚本,让脚本一直以后台模式运行到结束。可以用nohup命令来实现。
有了nohup,就可以在后台运行脚本。在无须停止脚本进程的情况下,登出终端会话去完成其他任务,随后再检查结果。
nohup命令能阻断发给特定进程的SIGHUP信号。当退出终端会话时,可以避免进程退出。
$ nohup ./testAscript.sh &
[1] 1828
$ nohup: ignoring input and appending output to 'nohup.out'.
使用 nohup命令时,如果关闭终端会话,则脚本会忽略其发送的 SIGHUP信号。
nohup 命令会解除终端与进程之间的关联,进程不再同 STDOUT和STDERR 绑定在一起。
为了保存该命令产生的输出,nohup命令会自动将 STDOUT和STDERR产生的消息重定向 到一个名为 nohup.out的文件中。
nohup.out文件一般在当前工作目录中创建,否则会在$HOME目录中创建。
当nohop 运行位于同一目录中的多个命令时,一定要当心,因为所有的命令输出都会发送到同一个nohup.out文件中.
4,作业控制
在作业停止后,Linux系统会让你选择是 “杀死” 该作业还是重启该作业.
要重启停止的进程,则需要向其发送SIGCONT信号。
4.1,查看作业 jobs
jobs是作业控制中的关键命令,该命令允许用户查看shell当前正在处理的作业。
$ cat jobcontrol.sh
#!/bin/bash
echo "Script Process ID: $$"
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 10
count=$[ $count + 1 ]
done
echo "End of script..."
exit
$ ./jobcontrol.sh
Script Process ID: 1580
Loop #1
Loop #2
Loop #3
^Z
[1]+ Stopped ./jobcontrol.sh
$ ./jobcontrol.sh > jobcontrol.out &
[2] 1603
$ jobs
[1]+ Stopped ./jobcontrol.sh
[2]- Running ./jobcontrol.sh > jobcontrol.out &
带有加号的作业为默认作业。如果作业控制命令没有指定作业号,则引用的就是该作业。
带有减号的作业会在默认作业结束之后成为下一个默认作业。不管shell中运行着多少作业,带加号的作业只能有一个,带减号的作业也只能有一个。
jobs命令的 -l选项(小写字母l)查看作业的PID
$ jobs -l
[1]+ 1580 Stopped ./jobcontrol.sh
[2]- 1603 Running ./jobcontrol.sh > jobcontrol.out &
jobs命令选项
选项 | 描述 |
---|---|
-l | 列出进程的PID以及作业号 |
-m | 只列出上次shell发出通知后状态发生改变的作业 |
-p | 只列出作业的PID |
-r | 只列出运行中的作业 |
-s | 只列出已停止的作业 |
删除已停止的作业,那么使用kill命令向其PID发送SIGKILL(9)信号
$ jobs -l
[1]+ 1580 Stopped ./jobcontrol.sh
$ kill -9 1580
[1]+ Killed ./jobcontrol.sh
4.2,重启已停止的作业 fg bg
在bash作业控制中,可以将已停止的作业作为后台进程 或 前台进程重启。前台进程会接管当前使用的终端,!!在使用该特性时要小心。
以后台模式重启作业,可以使用bg命令:
$ ./restartjob.sh
^Z
[1]+ Stopped ./restartjob.sh
$ bg
[1]+ ./restartjob.sh &
$ jobs
[1]+ Running ./restartjob.sh &
该作业是默认作业(从加号可以看出),所以仅使用bg命令就可以将其以后台模式重启。当作业被转入后台模式时,并不会显示其PID。
如果存在多个作业,则需要在bg命令后加上作业号,以便于控制:
$ jobs
$ ./restartjob.sh
^Z
[1]+ Stopped ./restartjob.sh
$ ./newrestartjob.sh
^Z
[2]+ Stopped ./newrestartjob.sh
$ bg 2
[2]+ ./newrestartjob.sh &
$ jobs
[1]+ Stopped ./restartjob.sh
[2]- Running ./newrestartjob.sh &
以前台模式重启作业,使用带有作业号的fg命令:
$ jobs
[1]+ Stopped ./restartjob.sh
[2]- Running ./newrestartjob.sh &
$ fg 2
./newrestartjob.sh
This is the script's end.
5,调整优先级(谦让度)
调度优先级是一个整数值,取值范围从 -20(最高优先级)到 +19(最低优先级)。在默认情况下,bash shell以优先级 0来启动所有进程。
5.1,nice命令 Nice guys finish last.
nice命令允许在启动命令时设置其调度优先级。
nice命令会阻止普通用户提高命令的优先级。即便提高其优先级的操作没有成功,指定的命令依然可以运行。只有root用户或者特权用户才能 提高 作业的优先级。
nice命令的-n选项指定新的优先级,-n选项并不是必需的,直接在 连字符后 跟上优先级也可以(优先级是负数时,容易造成混淆,出现了双连字符。最好还是使用-n选项)
$ nice -n 10 ./jobcontrol.sh > jobcontrol.out & 等同 nice -10 .....
[2] 16462
$ ps -p 16462 -o pid,ppid,ni,cmd
PID PPID NI CMD
16462 1630 10 /bin/bash ./jobcontrol.sh
ps命令的输出证实,谦让度(NI列)已经调整到了10。
5.2,renice命令
renice命令 修改系统中已运行命令的优先级。通过指定运行进程的PID来改变其优先级:
$ ./jobcontrol.sh > jobcontrol.out &
[2] 16642
$ ps -p 16642 -o pid,ppid,ni,cmd
PID PPID NI CMD
16642 1630 0 /bin/bash ./jobcontrol.sh
$ renice -n 10 -p 16642
16642 (process ID) old priority 0, new priority 10
$ ps -p 16642 -o pid,ppid,ni,cmd
PID PPID NI CMD
16642 1630 10 /bin/bash ./jobcontrol.sh
只能对对自己的进程使用 renice且只能降低调度优先级。root用户和特权用户可以使用 renice命令对任意进程的优先级做任意调整。
6,定时运行作业
希望脚本能在以后某个无法亲临现场的时候运行.
Linux系统提供了多个在预选时间运行脚本的方法:at命令、cron表 和 anacron。
6.1,使用at调度作业
at命令 指定Linux系统何时运行脚本。会将作业提交到队列中,指定shell何时运行该作业。
at的守护进程 atd在后台运行,在作业队列中检查待运行的作业。很多Linux发行版会在启动时运行此守护进程。
如果 Linux没有安装软件包,需要自行安装 软件包at。
6.1.1,at命令格式
at [-f filename] time
默认 at命令会将 STDIN 的输入放入队列。可以用 -f选项指定用于从中读取命令(脚本文件)的文件名。
time 选项指定了 希望何时运行该作业。如果指定的时间已经过去,会在 第二天 的同一时刻运行指定的作业。!!!!
at命令能识别多种时间格式: 具体查看 /usr/share/doc/at/timespec文件
·标准的小时和分钟,比如10:15。
·AM/PM指示符,比如10:15 PM。
·特定的时间名称,比如now、noon、midnight或者teatime(4:00 pm)。
·标准日期,比如 MMDDYY、MM/DD/YY或DD.MM.YY。
·文本日期,比如 Jul 4或 Dec 25,年份可有可无。
·时间增量。
·Now + 25 minutes
·10:15 PM tomorrow
·10:15 + 7 days
使用at命令时,作业会被提交至作业队列。作业队列保存着通过at命令提交的待处理作业。
针对不同优先级,有52种作业队列。作业队列通常用a~z和A~Z来指代,A队列 和 a队列是两个不同的队列。
作业队列的字母排序越高,此队列中的作业运行优先级就越低(谦让度更大)。
默认 at命令提交的作业会被放入a队列。
可以用 -q 选项指定其他的队列。如果相较于其他进程你希望你的作业尽可能少地占用CPU,可以将其放入z队列。
6.1.2,获取作业的输出
在 Linux系统中运行at命令时,显示器并不会关联到该作业。Linux系统 会将提交该作业的用户email地址作为 STDOUT和STDERR。
任何送往 STDOUT或 STDERR的输出都会通过邮件系统传给该用户。
at命令通过sendmail应用程序发送email。如果系统中没有安装sendmail,那就无法获得任何输出。因此在使用at命令时,最好在脚本中对STDOUT和STDERR进行重定向
$ cat tryatout.sh
#!/bin/bash
outfile=$HOME/scripts/tryat.out
echo "This script ran at $(date +%B%d,%T)" > $outfile
echo >> $outfile
echo "This script is using the $SHELL shell." >> $outfile
echo >> $outfile
sleep 5
echo "This is the script's end." >> $outfile
exit
$ at -M -f tryatout.sh now
warning: commands will be executed using /bin/sh
job 4 at Thu Jun 18 16:48:00 2020
$ cat $HOME/scripts/tryat.out
This script ran at June18,16:48:21
This script is using the /bin/bash shell.
This is the script's end.
不想在 at命令中使用 email或者重定向,则最好加上-M选项,以禁止作业产生的输出信息。
6.1.3,列出等待的命令 atq
atq命令可以查看系统中有哪些作业在等待
$ at -M -f tryatout.sh teatime
job 5 at Fri Jun 19 16:00:00 2020
$ at -M -f tryatout.sh tomorrow
job 6 at Fri Jun 19 16:53:00 2020
$ at -M -f tryatout.sh 20:30
job 7 at Thu Jun 18 20:30:00 2020
$ at -M -f tryatout.sh now+1hour
job 8 at Thu Jun 18 17:54:00 2020
$ atq
1 Thu Jun 18 16:11:00 2020 a christine
5 Fri Jun 19 16:00:00 2020 a christine
6 Fri Jun 19 16:53:00 2020 a christine
7 Thu Jun 18 20:30:00 2020 a christine
8 Thu Jun 18 17:54:00 2020 a christine
6.1.4,删除作业 atrm
可以用atrm命令 指定要删除的作业号,删除等待中的作业:
$ atq
1 Thu Jun 18 16:11:00 2020 a christine
5 Fri Jun 19 16:00:00 2020 a christine
$ atrm 5
$ atq
1 Thu Jun 18 16:11:00 2020 a christine
6.2,调度需要定期运行的脚本 cron
Linux系统使用cron程序调度需要定期执行的作业。cron在后台运行,并会检查 cron时间表,从中获知已安排执行的作业。
6.2.1,cron时间表
cron时间表通过一种特别的格式指定作业何时运行,其格式如下:
分 时 天 月 周 命令
minutepasthour hourofday dayofmonth month dayofweek command
cron时间表允许使用特定值、取值范围(比如1~5)或者通配符(*)来指定各个字段。
可以使用三字符的文本值(mon、tue、wed、thu、fri、sat、sun)或数值(0或7代表周日)来指定 dayofweek 字段。
如何设置才能让命令在每月的最后一天执行,因为无法设置一个 dayofmonth值(1~31).
涵盖所有月份的最后一天。常用的解决方法是加一个if-then语句,在其中使用date命令检查明天的日期是不是某个月份的第一天
00 00 28-31 * * if [ "$(date +%d -d tomorrow)" = 01 ] ; then command ; fi
命令列表必须指定要运行的命令或脚本的完整路径:
15 10 * * * /home/christine/backup.sh > backup.out
6.2.2,构建cron时间表 crontab
Linux提供了 crontab 命令来处理cron时间表。
列出已有的cron时间表,-l选项:
$ crontab -l
no crontab for christine
使用-e选项向cron时间表添加字段。 使用已有的cron时间表作为文件内容(如果时间表不存在,就是一个空文件)
6.2.3,浏览cron目录
创建的脚本对于执行时间的精确性要求不高,则用预配置的cron脚本目录会更方便。
预配置的基础目录共有4个:hourly、daily、monthly和 weekly。
将脚本复制到 daily 目录,cron就会每天运行.
$ ls /etc/cron.*ly
/etc/cron.daily:
0anacron apt-compat cracklib-runtime logrotate [...]
apport bsdmainutils dpkg man-db [...]
/etc/cron.hourly:
/etc/cron.monthly:
0anacron
/etc/cron.weekly:
0anacron man-db update-notifier-common
6.2.4,anacron程序
cron程序 村子的问题是 要求 Linux系统是7×24小时运行的.
如果某个作业在 cron时间表中设置的运行时间已到,但此时 Linux系统处于关闭状态,作业就不会运行。
当再次启动系统时,cron程序不会再运行错过的作业。
anacron判断出某个作业错过了设置的运行时间,会尽快运行该作业。能确保作业一定能运行.
anacron 程序只处理位于cron目录的程序。anacron 通过时间戳来判断作业是否在正确的计划间隔内运行了。
每个 cron目录都有一个时间戳文件,位于 /var/spool/anacron.
$ ls /var/spool/anacron
cron.daily cron.monthly cron.weekly
$ sudo cat /var/spool/anacron/cron.daily
20200619
anacron 程序使用自己的时间表(通常位于/etc/anacrontab)来检查作业目录:
$ cat /etc/anacrontab
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
HOME=/root
LOGNAME=root
# 替换cron的条目
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时间表的基本格式:
period delay identifier command
period 运行频率,anacron程序用 period 检查作业的时间戳文件。
delay 指定了在系统启动后,anacron程序需要等待多少分钟再开始运行错过的脚本。
identifier 独特的非空字符串,比如 cron.weekly,标识出现在日志消息和错误email中的作业。
command 包含了run-parts程序 和一个cron脚本目录名。run-parts程序负责运行指定目录中的所有脚本。
anacron 并不处理执行时间需求少于一天的脚本。不会运行位于/etc/cron.hourly目录的脚本。
7,使用新 shell启动脚本
希望为shell会话设置某些shell特性
用户登录bash shell的启动文件,(6)
$HOME/.bash_profile
$HOME/.bash_login
$HOME/.profile
并非所有发行版都包含这些启动文件.基本上所列文件中的第一个文件会被运行,其余的则会被忽略。
将需要在登录时运行的脚本放在 第一个文件 $HOME/.bash_profile 中。
每次启动新shell,bash shell都会运行 .bashrc 文件.
.bashrc文件通常也借由某个bash启动文件来运行,
.bashrc文件会运行两次:
当用户登录bash shell时,
当用户启动bash shell时
如果需要某个脚本在两个时刻都运行,可以将其放入.bashrc文件中。