Shell学习笔记(6)——控制脚本

控制信号

Linux利用信号与运行在系统中的进程进行通信。通过对脚本进行编程,使其在收到特定信号时执行某些命令,从而控制shell脚本的操作。Linux系统和应用程序可以生成超过30个信息,下表是常见的Linux系统信号:

常见的信号

信号 描述
1 SIGHUP 挂起进程
2 SIGINF 终止进程
3 SIGQUIT 停止进程
9 SIGKILL 无条件终止进程
15 SIGTERM 尽可能终止进程
17 SIGSTOP 无条件停止进程,但不是终止进程
18 SIGTSTP 停止或暂停进程,但不终止进程
19 SIGCONT 继续运行停止的进程

默认情况下,bash shell会忽略收到的任何 SIGQUIT (3) 和 SIGTERM (5) 信号(正因为这样,交互式shell才不会被意外终止)。但是bash shell会处理收到的 SIGHUP (1) 和 SIGINT (2) 信号。如果bash shell收到了SIGHUP信号,比如当你要离开一个交互式shell,它就会退出。但在退出之前,它会将SIGHUP信号传给所有由该shell所启动的进程(包括正在运行的shell脚本)。通过SIGINT信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这种情况发生时,shell会将SIGINT信号传给所有由它所启动的进程,以此告知出现的状况。你可能也注意到了,shell会将这些信号传给shell脚本程序来处理。而shell脚本的默认行为是忽略这些信号。它们可能会不利于脚本的运行。要避免这种情况,你可以脚本中加入识别信号的代码,并执行命令来处理信号。

生成信号

使用键盘上的组合键可以生成2种基本的Linux信号。

中断进程

Ctrl+C会生成SIGINT`信号,将其发送给当前在shell运行中的所有进程,停止shell当前运行的进程,如下所示:

1
2
3
biotest@ubuntu:~/input/dir.4ZeP0T$ sleep 500
^C
biotest@ubuntu:~/input/dir.4ZeP0T$

暂停进程

Ctrl+Z可以生成一个SIGTSTP信号,停止shell运行的任何进程,停止进程(stopping)与终止(terminating)进程不同,停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行,如下所示:

1
2
3
biotest@ubuntu:~/input/dir.4ZeP0T$ sleep 100
^Z
[1]+ Stopped sleep 100

结果中的方括号里的数字是shell分配的作业号(job number),shell将运行中的每个进程称为作为,并为每个作业分配有唯一的作业号,第一个作业分配的是作业号1,第二个是2。如果shell会话中有一个已经停止的作业,在退出shell时,bash会提醒用户,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/input/dir.4ZeP0T$ sleep 100
^Z
[1]+ Stopped sleep 100
biotest@ubuntu:~/input/dir.4ZeP0T$ ^C
biotest@ubuntu:~/input/dir.4ZeP0T$ exit
exit
There are stopped jobs.

通过ps命令可以查看已经停止的作业,如下所示:

1
2
3
4
5
biotest@ubuntu:~/input/dir.4ZeP0T$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 10009 10002 0 80 0 - 5700 wait pts/4 00:00:01 bash
0 T 1000 28664 10009 0 80 0 - 1822 signal pts/4 00:00:00 sleep
0 R 1000 28674 10009 0 80 0 - 7229 - pts/4 00:00:00 ps

在S列(进程状态),ps命令将已经停止作业的状态显示为T,这说明命令要么被跟踪,要么被停止了。如果在有已停止作业存在的情况下,你仍旧想退出shell,只要再输入一遍 exit 命令就行了。shell会退出,终止已停止作业。或者,既然你已经知道了已停止作业的PID,就可以用 kill 命令来发送一个 SIGKILL 信号来终止它(-9表示强迫终止)。

1
2
biotest@ubuntu:~/input/dir.4ZeP0T$ kill -9 28664
[1]+ Killed sleep 100

捕获信号

在信号出现时捕获它们并执行其他命令时用trap,此命令可以指定shell脚本要监视并从shell中拦截的Linux信号,如果脚本收到了trap命令中列出的信号,此信号不再由shell处理,则是交给本地处理,trap的用法为trap commands signals,如下所示:

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

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
biotest@ubuntu:~/signal$ bash test1.sh
This is a test script
Loop #1
Loop #2
Loop #3
^C Sorry! I have trapped Ctrl-C
Loop #4
Loop #5
^C Sorry! I have trapped Ctrl-C
Loop #6
^C Sorry! I have trapped Ctrl-C
Loop #7
^C Sorry! I have trapped Ctrl-C
Loop #8
^C Sorry! I have trapped Ctrl-C
Loop #9
Loop #10
This is the end of the test script

在这个案例中,使用了trap命令,在检测到SIGINT信号时,就会显示一行简单的文本消息,捕获这些信号会阻止用户使用bash shell组合键Ctrl+C来停止程序。也就是说每次使用组合键Ctrl+C时,脚本就都会执行trap命令中指定的echo语句,而不是处理该信号并允许shell停止该脚本。

捕获脚本退出

shell脚本在退出时也能进行捕获,这是在shell完成任务时执行命令的一种简便方法,用法是在trap命令后加上EXIT信号,如下所示:

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

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/signal$ bash test2.sh
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Goodbye...
biotest@ubuntu:~/signal$ bash test2.sh
Loop #1
Loop #2
Loop #3
^CGoodbye...

当脚本运行到正常的退出位置时,捕获就被触发了,shell会执行在trap命令行指定的命令。如果提前退出脚本,同样能够捕获到EXIT。这是因为SIGINT信号并没有出现在trap命令的捕获列表中,当按下Ctrl+C组合键发送SIGINT信号时,脚本就退出了。但在脚本退出前捕获到了EXIT,于是shell执行了trap命令。

修改或移除捕获

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

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

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/signal$ bash test3.sh
Loop #1
Loop #2
^C Sorry...Ctrl-C is trapped.
Loop #3
Loop #4
Loop #5
Second Loop #1
^CI modified the trap!
Second Loop #2
Second Loop #3
Second Loop #4
Second Loop #5

修改了信号捕获之后,脚本处理信号的方式就会发生变化。但如果一个信号是在捕获被修改前接收到的,那么脚本仍然会根据最初的 trap 命令进行处理。 也可以删除已设置好的捕获。只需要在 trap 命令与希望恢复默认行为的信号列表之间加上两个破折号就行了。如下所示:

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

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/signal$ bash test3b.sh
Loop #1
Loop #2
^C Sorry...Ctrl-C is trapped.
Loop #3
Loop #4
Loop #5
I just removed the trap
Second Loop #1
Second Loop #2
^C

移除信号捕获后,脚本按照默认行为来处理 SIGINT 信号,也就是终止脚本运行。但如果信号是在捕获被移除前接收到的,那么脚本会按照原先 trap 命令中的设置进行处理。 在本例中,第一个Ctrl+C组合键用于提前终止脚本。因为信号在捕获被移除前已经接收到了,脚本会照旧执行 trap 中指定的命令。捕获随后被移除,再按Ctrl+C就能够提前终止脚本了。

后台模式运行脚本

有些脚本不会在终端的显示器上显示,而是在后台(background)运行,在后台模式中,进行运行不会与STDIN、STDOUT以及STDERR发生关联。

后台运行脚本

打开后台模式运行脚本霜肆在脚本后面添加上&符号即可,如下所示:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# test running in the background
#
count=1
while [ $count -le 10 ]
do
sleep 1
count=$[ $count + 1 ]
done

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/signal$ bash test4.sh &
[1] 28920
biotest@ubuntu:~/signal$ ps
PID TTY TIME CMD
10009 pts/4 00:00:01 bash
28920 pts/4 00:00:00 bash
28923 pts/4 00:00:00 sleep
28924 pts/4 00:00:00 ps
# 按回车键
biotest@ubuntu:~/signal$
[1]+ Done bash test4.sh

从结果可以看出,当 & 符放到命令后时,它会将命令和bash shell分离开来,将命令作为系统中的一个独立的后
台进程运行。显示的第一行是[1] 28920,其中[1]是shell分配给后台进程的作业号。28920是进程号。在运行结束后,按回车键,脚本会在终端显示消息([1]+ Done...),就是结果的最后一部分,这表明了作业的作业号以及作业状态(Done)。当脚本在后台运行时,它仍然会使用终端显示器来显示STDOUT和STDERR,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# Test running in the background with output
#
#
echo "Start the test script"
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 5
count=$[ $count+1 ]
done
#
echo "Test script is complete"
#

结果运行如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/signal$ bash test5.sh &
[1] 28986
biotest@ubuntu:~/signal$ Start the test script
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Test script is complete
[1]+ Done bash test5.sh

在这个案例中,脚本test5.sh的输出与shell提示符混杂在一歧异,这也是为什么Start the test script会出现在提示符普边的原因。在显示输出的同时,仍然可以运行命令,如下所示:

1
2
3
4
biotest@ubuntu:~/signal$ Start the test script
Loop #1
ls
test1.sh test2.sh test3b.sh test3.sh test4.sh test5.sh

当脚本test5.sh运行在后台模式时,输入了命令ls。脚本输出、输入的命令以及命令输出全都混在了一起。针对这种情况,最好是将后台运行的脚本的STDOUT和STDERR进行重定向,避免这种杂乱的输出。

运行多个后台作业

可以在命令行提示符下同时启动多个后台作业,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
biotest@ubuntu:~/signal$ bash test6.sh &
[2] 29019
[1] Done bash test5.sh
biotest@ubuntu:~/signal$ bash: test6.sh: No such file or directory
bash test7.sh &
[3] 29020
[2] Exit 127 bash test6.sh
biotest@ubuntu:~/signal$ bash: test7.sh: No such file or directory
bash test8.sh &
[4] 29021
[3] Exit 127 bash test7.sh
biotest@ubuntu:~/signal$ bash: test8.sh: No such file or directory
ps
PID TTY TIME CMD
10009 pts/4 00:00:01 bash
29022 pts/4 00:00:00 ps
[4]+ Exit 127 bash test8.sh

每次启动新作业时,Linux系统都会为其分配一个新的作业号和PID。通过 ps 命令,可以看到所有脚本处于运行状态。 在终端会话中使用后台进程时一定要小心。注意,在ps命令的输出中,每一个后台进程都和终端会话(pts/0)终端联系在一起。如果终端会话退出,那么后台进程也会随之退出。

nohup命令

在有些情况下,用户会终端会话中启动shell脚本,即使退出了终端会话,脚本也会一直在后台模式运行,直到结束。这可以用nohup命令来实现。nohup命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP信号。这会在退出终端会话时阻止进程退出。nohup命令的格式为nohup commands &,如下所示:

1
2
3
biotest@ubuntu:~/signal$ nohup bash test1.sh &
[1] 29325
biotest@ubuntu:~/signal$ nohup: ignoring input and appending output to 'nohup.out'

shell会给命令分配一个作业号,Linux系统会为其分配一个PID号。与shell中的普通进程相比,区 别在于,当使用nohup命令时,如果关闭该会话,脚本会忽略终端会话发过来的SIGHUP信号。 由于nohup命令会解除终端与进程的关联,进程也就不再同STDOUT和STDERR联系在一起。 为了保存该命令产生的输出,nohup命令会自动将STDOUT和STDERR的消息重定向到一个名为 nohup.out的文件中。如果使用nohup运行了另一个命令,该命令的输出会被追加到已有的nohup.out文件中。因此,当运行位于同一个目录中的多个命令时一定要小心,因为所有的输出都会被发送到同一个nohup.out文件中,结果会让人摸不清头脑。

nohup.out文件包含了通常会发送到终端显示器上的所有输出。在进程完成运行后,你可以查 看nohup.out文件中的输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
biotest@ubuntu:~/signal$ ls
nohup.out test1.sh test2.sh test3b.sh test3.sh test4.sh test5.sh
biotest@ubuntu:~/signal$ cat nohup.out
This is a test script
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Loop #6
Loop #7
Loop #8
Loop #9
Loop #10
This is the end of the test script

作业控制

在作业停止后,Linux系统会让用户选择是终止还是重启。用户可以用kill命令终止该进程。要重启停止的进程需要向其发送一个SIGCONT信号。启动、停止、终止以及恢复作业的这些功能统称为作业控制。通过作业控制,就能完全控制shell环境中所有进程的运行方式。

查看作业

查看作业的命令是jobs,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# test job control
echo "Script process ID: $$"
count=1
while [ $count -le 10 ]
do
echo "Loop #$count"
sleep 10
count=$[ $count+1 ]
done
echo "End of script ..."

运行结果,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/signal$ bash test10.sh
Script process ID: 29360
Loop #1
^Z
[1]+ Stopped bash test10.sh
biotest@ubuntu:~/signal$ bash test10.sh > test10.out &
[2] 29364
biotest@ubuntu:~/signal$ jobs
[1]+ Stopped bash test10.sh
[2]- Running bash test10.sh > test10.out &
biotest@ubuntu:~/signal$ jobs -l
[1]+ 29360 Stopped bash test10.sh
[2]- 29364 Running bash test10.sh > test10.out &

代码及结果解释:脚本使用$$$$变量业显示Linux系统分配给此脚本的PID,然后进入循环,每次迭代都休眠10秒,可以从命令行中启动脚本,然后使用Ctrl+Z停止脚本。随后又使用了&操作将另外一个作用设为后台进程启动,出于简化目的,在这个案例中,脚本的输出被重定向到文件中,避免出现在屏幕上。jobs命令可以查看分配给Shell的作用,jobs命令显示了2个已经停止/运行中的作用,以及它们的作业号和作业中使用的命令。如果使用job -l则会业显示作用的PID,有关jobs命令的一些参数,如下表所示:

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

jobs命令输出中有加号和减号。带加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。当前的默认作业完成处理后,带减号的作业成为下一个默认作业。任何时候,不管shell中有多少个正在运行的作业,shell中都只有一个带加号的作业和一个带减号的作业。

下面例子说明了队列中的下一个作业在默认作业移除时是如何成为默认作业的。有3个独立的进程在后台被启动。jobs命令显示出了这些进程、进程的PID及其状态。注意,默认进程(带有加号的那个)是最后启动的那个进程,也就是3号作业。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
biotest@ubuntu:~/signal$ bash test10.sh > test10a.out &
[2] 29402
biotest@ubuntu:~/signal$ bash test10.sh > test10.out &
[3] 29404
biotest@ubuntu:~/signal$ bash test10.sh > test10c.out &
[4] 29409
biotest@ubuntu:~/signal$ jobs -l
[2]+ 29402 Running bash test10.sh > test10a.out &
[3] 29404 Running bash test10.sh > test10.out &
[4]- 29409 Running bash test10.sh > test10c.out &
biotest@ubuntu:~/signal$ kill 29402
biotest@ubuntu:~/signal$ jobs -l
[3]+ 29404 Running bash test10.sh > test10.out &
[4]- 29409 Terminated bash test10.sh > test10c.out
biotest@ubuntu:~/signal$ kill 29404
# 调用了 kill 命令向默认进程发送了一个 SIGHUP 信号,终止了该作业。在接下来的 jobs
# 命令输出中,先前带有减号的作业成了现在的默认作业,减号也变成了加号。
biotest@ubuntu:~/signal$ jobs -l
[3]+ 29409 Terminated bash test10.sh > test10.out

重启停止的作业

在bash作业控制中,可以将已停止的作业作为后台进程或前台进程重启。前台进程会接管你当前工作的终端,所以在使用该功能时要小心,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/signal$ bash test10.sh
Script process ID: 29612
Loop #1
^Z
[1]+ Stopped bash test10.sh
biotest@ubuntu:~/signal$ bg
[1]+ bash test10.sh &
biotest@ubuntu:~/signal$ jobs
[1]+ Running bash test10.sh &

因为该作业是默认作业(从加号可以看出),只需要使用bg命令就可以将其以后台模式重启。 注意,当作业被转入后台模式时,并不会列出其PID。如果有多个作业,需要在bg命令后加上作业号,如果要以前台模式恢复运行,则用fg加上作业号。

 调整优先级

在多任务操作系统中(Linux就是),内核负责将CPU时间分配给系统上运行的每个进程。调度优先级(scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)。在Linux系统中,由shell启动的所有进程的调度优先级默认都是相同的。调度优先级是个整数值,从20(最高优先级)到+19(最低优先级)。默认情况下,bashshell以优先级0来启动所有进程。

有时用户要改变一个shell脚本的优先级。不管是降低它的优先级(这样它就不会从占用其他 进程过多的处理能力),还是给予它更高的优先级(这样它就能获得更多的处理时间),都可以通过nice这个命令达到目的。

nice命令

nice命令允许你设置命令启动时的调度优先级。要让命令以更低的优先级运行,只要用nice的-n命令行来指定新的优先级级别。

1
2
3
4
5
biotest@ubuntu:~/signal$ nice -n 10 bash test4.sh > test4.out &
[2] 29646
biotest@ubuntu:~/signal$ ps -p 29646 -o pid,ppid,ni,cmd
PID PPID NI CMD
[2]- Done nice -n 10 bash test4.sh > test4.out

注意,必须将nice命令和要启动的命令放在同一行中。ps命令的输出验证了谦让度值(NI列)已经被调整到了10。

nice命令会让脚本以更低的优先级运行。但如果想提高某个命令的优先级,普通用户则无法实现,如下所示:

1
2
3
biotest@ubuntu:~/signal$ nice -n -10 bash test4.sh > test4.out &
[2] 29677
biotest@ubuntu:~/signal$ nice: cannot set niceness: Permission denied

nice命令阻止普通系统用户来提高命令的优先级。注意,指定的作业的确运行了,但是试图使用nice命令提高其优先级的操作却失败了,这很好,理解计算机的资源是有限有,用户可以将自己的作业推后,但是你不能提前。nice命令的-n选项并不是必须的,只需要在破折号后面跟上优先级就行了。

1
2
3
4
5
6
biotest@ubuntu:~/signal$ nice -10 bash test4.sh > test4.out &
[3] 29691
[2] Done nice -n -10 bash test4.sh > test4.out
biotest@ubuntu:~/signal$ ps -p 29691 -o pid,ppid,ni,cmd
PID PPID NI CMD
[3]- Done nice -10 bash test4.sh > test4.out

renice命令

renice命令可以改变系统上已经运行命令的优先级,它允许用户指定运行进程的PID来改变它的优先级,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/signal$ bash test10.sh &
[2] 29718
biotest@ubuntu:~/signal$ ps -p 29718 -o pid,Loop #3
ppid,ni,cmd
PID PPID NI CMD
29718 10009 0 bash test10.sh
biotest@ubuntu:~/signal$ renice -n 10 -p 29718
29718 (process ID) old priority 0, new priority 10
biotest@ubuntu:~/signal$ ps -p 29718 -o pid,ppid,ni,cmd
PID PPID NI CMD
29718 10009 10 bash test10.sh

renice命令会自动更新当前运行进程的调度优先级。和nice命令一样,renice命令也有一些限制:第一,只能对属于用户自身的进程执行renice;只能通过renice降低进程的优先级;root用户可以通过renice来任意调整进程的优先级。如果想完全控制运行进程,必须以root账户身份登录或使用sudo命令。

定时运行作业

at命令和cron表可以指定用户在某个时间运行脚本。

at命令

at命令允许指定Linux系统何时运行脚本。at命令会将作业提交到队列中,指定shell何时运行该作业。at的守护进程atd会以后台模式运行,检查作业队列来运行作业。atd守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at)来获取用at命令提交的作业。默认情况下,atd守护进程会每60秒检查一下这个目录。有作业时,atd守护进程会检查作业设置运行的时间。如果时间跟当前时间匹配,atd守护进程就会运行此作业。

at命令的格式

at命令的格式at命令的基本格式为:at [-f filename ] time

默认情况下,at命令会将STDIN的输入放到队列中。用户可以用-f参数来指定用于读取命令(脚本文件)的文件名。time参数指定了Linux系统何时运行该作业。如果你指定的时间已经错过,at命令会在第二天的那个时间运行指定的作业。at命令能识别多种不同的时间格式。

  1. 标准的小时和分钟格式,比如10:15。
  2. AM/PM指示符,比如10:15 PM。
  3. 特定可命名时间,比如now、noon、midnight或者teatime(4 PM)。
    除了指定运行作业的时间,也可以通过不同的日期格式指定特定的日期。
  4. 标准日期格式,比如MMDDYY、MM/DD/YY或DD.MM.YY。
  5. 文本日期,比如Jul 4或Dec 25,加不加年份均可。
  6. 可以指定时间增量(例如当前时间+25min;明天10:15 PM;10:15 + 7days)。

在使用at命令时,该作业会被提交到作业队列(jobqueue)。作业队列会保存通过at命令提交的待处理的作业。针对不同优先级,存在26种不同的作业队列。作业队列通常用小写字母a~z和大写字母A~Z来指代。作业队列的字母排序越高,作业运行的优先级就越低(更高的nice值)。默认情况下,at的作业会被提交到a作业队列。如果想以更高优先级运行作业,可以用-q参数指定不同的队列字母。

获取作业的输出

当作业在Linux系统上运行时,显示器并不会关联到该作业。取而代之的是,Linux系统会将提交该作业的用户的电子邮件地址作为STDOUT和STDERR。任何发到STDOUT或STDERR的输出都会通过邮件系统发送给该用户。看一个案例:

1
2

注:Ubuntu上可能没有安装at,需要自己手动安装。at命令会显示分配给作业的作业号以及为作业安排的运行时间。-f选项指明使用哪个脚本 文件,now指示at命令立刻执行该脚本。

使用e-mail作为at命令的输出极其不便。at命令利用sendmail应用程序来发送邮件。如果你的系统中没有安装sendmail,那就无法获得任何输出,因此在使用at命令时,最好在脚本中对STDOUT和STDERR进行重定向,如下例所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# Test using at command
echo "This script ran at $(date +%B%d,%T)" > test13b.out
echo >> test13b.out
sleep 5
echo "This is the script's end ..." >> test13b.out
biotest@ubuntu:~/signal$ chmod u+x test13b.sh
biotest@ubuntu:~/signal$ at -M -f test13b.sh now
warning: commands will be executed using /bin/sh
job 3 at Sun May 6 05:10:00 2018
biotest@ubuntu:~/signal$ cat test13b.out
This script ran at May06,05:10:51
This is the script's end ...

如果不想在at命令中使用邮件或重定向,最好加上-M选项来屏蔽作业产生的输出信息。

列出等待的作业

atq命令可以查看系统中有哪些作业在等待,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
biotest@ubuntu:~/signal$ at -M -f test13b.sh teatime
warning: commands will be executed using /bin/sh
job 4 at Sun May 6 16:00:00 2018
biotest@ubuntu:~/signal$ at -M -f test13b.sh tomorrow
warning: commands will be executed using /bin/sh
job 5 at Mon May 7 05:16:00 2018
biotest@ubuntu:~/signal$ at -M -f test13b.sh 13:30
warning: commands will be executed using /bin/sh
job 6 at Sun May 6 13:30:00 2018
biotest@ubuntu:~/signal$ at -M -f test13b.sh now
warning: commands will be executed using /bin/sh
job 7 at Sun May 6 05:16:00 2018
biotest@ubuntu:~/signal$ atq
4 Sun May 6 16:00:00 2018 a biotest
1 Sun May 6 05:06:00 2018 = biotest
5 Mon May 7 05:16:00 2018 a biotest
2 Sun May 6 05:06:00 2018 = biotest
7 Sun May 6 05:16:00 2018 = biotest
6 Sun May 6 13:30:00 2018 a biotest

作业列表中显示了作业号、系统运行该作业的日期和时间及其所在的作业队列。

删除作业

一旦知道了哪些作业在作业队列中等待,就能用 atrm 命令来删除等待中的作业。

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/signal$ atq
4 Sun May 6 16:00:00 2018 a biotest
1 Sun May 6 05:06:00 2018 = biotest
5 Mon May 7 05:16:00 2018 a biotest
2 Sun May 6 05:06:00 2018 = biotest
6 Sun May 6 13:30:00 2018 a biotest
biotest@ubuntu:~/signal$ atrm 2
Warning: deleting running job
biotest@ubuntu:~/signal$ atq
4 Sun May 6 16:00:00 2018 a biotest
1 Sun May 6 05:06:00 2018 = biotest
5 Mon May 7 05:16:00 2018 a biotest
6 Sun May 6 13:30:00 2018 a biotest

cron时间表

如果用户需要脚本在每天的同一时间运行或是每周一次、每月一次运行,就要使用cron程序来安排要定期执行的作业。cron程序会在后台运行并检查一个特殊的表(被称作cron时间表),以获知已安排执行的作业。

cron时间表

  1. cron时间表采用一种特别的格式来指定作业何时运行。其格式为min hour dayofmonth month dayofweek command,cron时间表允许你用特定值、取值范围(比如1~5)或者是通配符(星号)来指定条目。
  2. 例如,如果想在每天的10:15运行一个命令,可以用cron时间表条目:15 10 * * * command,在dayofmonthmonth以及dayofweek字段中使用了通配符,表明cron会在每个月每天的10:15执行该命令。要指定在每周一4:15PM运行的命令,可以用这样的命令,即15 16 * * 1 command
  3. 可以用三字符的文本值(mon、tue、wed、thu、fri、sat、sun)或数值(0为周日,6为周六)来指定dayofweek表项。 再看一个例子,如果要在每个月的第一天中午12点执行命令。可以用这样的命令,即00 12 1 * * command,dayofmonth表项指定月份中的日期值(1~31)。
  4. 如何设置一个在每个月的最后一天执行的命令,因为无法设置dayofmonth的值来涵盖所有的月份。常用的方法是加一条使用date 命令的 if-then 语句来检查明天的日期是不是01,如下所示:
1
00 12 * * * if [ ` date +%d -d tomorrow ` = 01 ] ; then ; command

上述这段代码会在每天中午12点来检查是不是当月的最后一天,如果是,cron将会运行该命令。

  1. 命令列表必须指定要运行的命令或脚本的全路径名。你可以像在普通的命令行中那样,添加任何想要的命令行参数和重定向符号。 15 10 * * * /home/biotest/test4.sh > test4out,cron程序会用提交作业的用户账户运行该脚本。因此,用户必须有访问该命令和命令中指定的输出文件的权限。

构建cron时间表

每个系统用户(包括root用户)都可以用自己的cron时间表来运行安排好的任务。Linux提供了crontab命令来处理cron时间表。要列出已有的cron时间表,可以用-l选项,如下所示:

1
2
biotest@ubuntu:~/signal$ crontab -l
no crontab for biotest

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

浏览cron目录

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

anacron程序

cron程序的唯一问题是它假定Linux系统是7×24小时运行的。如果某个作业在cron时间表中安排运行的时间已到,但这时候Linux系统处于关机状态,那么这个作业就不会被运行。当系统开机时,cron程序不会再去运行那些错过的作业。要解决这个问题,许多Linux发行版还包含了anacron程序。如果anacron知道某个作业错过了执行时间,它会尽快运行该作业。这意味着如果Linux系统关机了几天,当它再次开机时,原定在关机期间运行的作业会自动运行。这个功能常用于进行常规日志维护的脚本。如果系统在脚本应该运行的时间刚好关机,日志文件就不会被整理,可能会变很大。通过anacron,至少可以保证系统每次启动时整理日志文件。anacron程序只会处理位于cron目录的程序,比如/etc/cron.monthly。它用时间戳来决定作业是否在正确的计划间隔内运行了。每个cron目录都有个时间戳文件,该文件位于/var/spool/anacron,如下所示:

1
2
3
biotest@ubuntu:~/signal$ sudo cat /var/spool/anacron/cron.monthly
[sudo] password for biotest:
20180427

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/signal$ sudo cat /etc/anacrontab
# /etc/anacrontab: configuration file for anacron
# See anacron(8) and anacrontab(5) for details.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
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时间表略有不同,前者的格式为period delay identifier command。

period条目定义了作业多久运行一次,以天为单位。anacron程序用此条目来检查作业的时间戳文件。delay条目会指定系统启动后anacron程序需要等待多少分钟再开始运行错过的脚本。command条目包含了run-parts程序和一个cron脚本目录名。run-parts程序负责运行目录中传给它的任何脚本。注意,anacron不会运行位于/etc/cron.hourly的脚本。这是因为anacron程序不会处理执行时间需求小于一天的脚本。identifier条目是一种特别的非空字符串,如cron-weekly。它用于唯一标识日志消息和错误邮件中的作业。

使用新 shell 启动脚本

如果每次运行脚本的时候都能够启动一个新的bashshell(即便只是某个用户启动了一个bashshell),将会非常的方便。有时候,用户希望为shell会话设置某些shell功能,或者只是为了确保已经设置了某个文件。用户登入bashshell时需要运行的启动文件基本上依照下列顺序所找到的第一个文件会被运行,其余的文件会被忽略:

1
2
3
$HOME/.bash_profile
$HOME/.bash_login
$HOME/.profile

因此,应该将需要在登录时运行的脚本放在上面第一个文件中。每次启动一个新shell时,bash shell都会运行.bashrc文件。可以这样来验证:在主目录下的.bashrc文件中加入一条简单的echo语句,然后启动一个新shell。

1
2
3
4
5
6
# source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# User specific aliases and functions
echo "I'm in a new shell!"

运行结果如下所示:

1
2
3
4
biotest@ubuntu:~$ bash
I'm in a new shell!
biotest@ubuntu:~$ exit
exit

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