天天看点

控制bash脚本

处理信号

Linux利用信号与运行在系统中的进程进行通信。可以通过对脚本进行编程,使其在收到特定信号时执行某些命令,从而控制shell脚本的操作。

重温 Linux 信号

Linux系统和应用程序可以生成超过30个信号。表16-1列出了在Linux编程时会遇到的最常见的Linux系统信号。

表16-1 Linux信号

信 号 描 述
1 SIGHUP 挂起进程
2 SIGINT 终止进程
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脚本的默认行为是忽略这些信号。它们可能会不利于脚本的运行。要避免这种情况,你可以脚本中加入识别信号的代码,并执行命令来处理信号。

生成信号

bash shell允许用键盘上的组合键生成两种基本的Linux信号。这个特性在需要停止或暂停失控程序时非常方便。

中断进程

Ctrl+C组合键会生成SIGINT信号,并将其发送给当前在shell中运行的所有进程。可以运行一条需要很长时间才能完成的命令,然后按下Ctrl+C组合键来测试它。

$ sleep 100
^C
           

Ctrl+C组合键会发送SIGINT信号,停止shell中当前运行的进程。 sleep命令会使得shell暂停指定的秒数,命令提示符直到计时器超时才会返回。在超时前按下Ctrl+C组合键,就可以提前终止sleep命令。

暂停进程

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

Ctrl+Z组合键会生成一个SIGTSTP信号,停止shell中运行的任何进程。停止(stopping)进程跟终止( terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行。

当用Ctrl+Z组合键时, shell会通知你进程已经被停止了。

$ sleep 100
^Z
[1]+ Stopped sleep 100
           

方括号中的数字是shell分配的作业号( job number)。 shell将shell中运行的每个进程称为作业,并为每个作业分配唯一的作业号。它会给第一个作业分配作业号1,第二个作业号2,以此类推。

如果你的shell会话中有一个已停止的作业,在退出shell时, bash会提醒你。

$ sleep 100
^Z
[1]+ Stopped sleep 100
$ exit
exit
There are stopped jobs.
           

可以用ps命令来查看已停止的作业。

$ sleep 100
^Z
[1]+ Stopped sleep 100
$
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 501 2431 2430 0 80 0 - 27118 wait pts/0 00:00:00 bash
0 T 501 2456 2431 0 80 0 - 25227 signal pts/0 00:00:00 sleep
0 R 501 2458 2431 0 80 0 - 27034 - pts/0 00:00:00 ps
           

在S列中(进程状态), ps命令将已停止作业的状态为显示为T。这说明命令要么被跟踪,要么被停止了。

如果在有已停止作业存在的情况下,你仍旧想退出shell,只要再输入一遍exit命令就行了。

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

$ kill -9 2456
$
[1]+ Killed sleep 100
           

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

捕获信号

也可以不忽略信号,在信号出现时捕获它们并执行其他命令。 trap命令允许你来指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再由shell处理,而是交由本地处理。

trap命令的格式是:

trap commands signals
           

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

这里有个简单例子,展示了如何使用trap命令来忽略SIGINT信号,并控制脚本的行为。

$ cat test1.sh
#!/bin/bash
# Testing signal 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"
           

本例中用到的trap命令会在每次检测到SIGINT信号时显示一行简单的文本消息。捕获这些信号会阻止用户用bash shell组合键Ctrl+C来停止程序。

$ ./test1.sh
This is a test script
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
^C Sorry! I have trapped Ctrl-C
Loop #6
Loop #7
Loop #8
^C Sorry! I have trapped Ctrl-C
Loop #9
Loop #10
This is the end of the test script
           

每次使用Ctrl+C组合键,脚本都会执行trap命令中指定的echo语句,而不是处理该信号并允许shell停止该脚本。

捕获脚本退出

除了在shell脚本中捕获信号,你也可以在shell脚本退出时进行捕获。这是在shell完成任务时执行命令的一种简便方法。

要捕获shell脚本的退出,只要在trap命令后加上EXIT信号就行。

$ cat test2.sh
#!/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
#
$
$ ./test2.sh
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Goodbye...
           

当脚本运行到正常的退出位置时,捕获就被触发了, shell会执行在trap命令行指定的命令。

如果提前退出脚本,同样能够捕获到EXIT。

$ ./test2.sh
Loop #1
Loop #2
Loop #3
^CGoodbye...
           

因为SIGINT信号并没有出现在trap命令的捕获列表中,当按下Ctrl+C组合键发送SIGINT信号时,脚本就退出了。但在脚本退出前捕获到了EXIT,于是shell执行了trap命令。

修改或移除捕获

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

$ cat test3.sh
#!/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
#
           

修改了信号捕获之后,脚本处理信号的方式就会发生变化。但如果一个信号是在捕获被修改前接收到的,那么脚本仍然会根据最初的trap命令进行处理。

$ ./test3.sh
Loop #1
Loop #2
Loop #3
^C Sorry... Ctrl-C is trapped.
Loop #4
Loop #5
Second Loop #1
Second Loop #2
^C I modified the trap!
Second Loop #3
Second Loop #4
Second Loop #5
           

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

$ cat test3b.sh
#!/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
#
$ ./test3b.sh
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
I just removed the trap
Second Loop #1
Second Loop #2
Second Loop #3
^C
           
窍门 也可以在trap命令后使用单破折号来恢复信号的默认行为。单破折号和双破折号都可以 正常发挥作用。

移除信号捕获后,脚本按照默认行为来处理SIGINT信号,也就是终止脚本运行。但如果信号是在捕获被移除前接收到的,那么脚本会按照原先trap命令中的设置进行处理。

$ ./test3b.sh
Loop #1
Loop #2
Loop #3
^C Sorry... Ctrl-C is trapped.
Loop #4
Loop #5
I just removed the trap
Second Loop #1
Second Loop #2
^C
           

在本例中, 第一个Ctrl+C组合键用于提前终止脚本。因为信号在捕获被移除前已经接收到了,脚本会照旧执行trap中指定的命令。捕获随后被移除,再按Ctrl+C就能够提前终止脚本了。

以后台模式运行脚本

直接在命令行界面运行shell脚本有时不怎么方便。一些脚本可能要执行很长一段时间,而你可能不想在命令行界面一直干等着。当脚本在运行时,你没法在终端会话里做别的事情。幸好有个简单的方法可以解决。

在用ps命令时,会看到运行在Linux系统上的一系列不同进程。显然,所有这些进程都不是运行在你的终端显示器上的。这样的现象被称为在后台(background)运行进程。在后台模式中,进程运行时不会和终端会话上的STDIN、 STDOUT以及STDERR关联。

也可以在shell脚本中试试这个特性,允许它们在后台运行而不用占用终端会话。下面几节将会介绍如何在Linux系统上以后台模式运行脚本。

后台运行脚本

以后台模式运行shell脚本非常简单。只要在命令后加个&符就行了。

$ cat test4.sh
#!/bin/bash
# Test running in the background
#
count=1
while [ $count -le 10 ]
do
	sleep 1
	count=$[ $count + 1 ]
done
#
$
$ ./test4.sh &
[1] 3231
           

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

方括号中的数字是shell分配给后台进程的作业号。下一个数是Linux系统分配给进程的进程ID(PID)。 Linux系统上运行的每个进程都必须有一个唯一的PID。

一旦系统显示了这些内容,新的命令行界面提示符就出现了。你可以回到shell,而你所执行的命令正在以后台模式安全的运行。这时,你可以在提示符输入新的命令当后台进程结束时,它会在终端上显示出一条消息:

[1] Done ./test4.sh
           

这表明了作业的作业号以及作业状态( Done),还有用于启动作业的命令。

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

$ cat test5.sh
#!/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"
#
$
$ ./test5.sh &
[1] 3275
$ Start the test script
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Test script is complete
[1] Done ./test5.sh
           

你会注意到在上面的例子中,脚本test5.sh的输出与shell提示符混杂在了一起,这也是为什么

Start the test script会出现在提示符旁边的原因。

在显示输出的同时,你仍然可以运行命令。

$ ./test5.sh &
[1] 3319
$ Start the test script
Loop #1
Loop #2
Loop #3
ls myprog*
myprog myprog.c
$ Loop #4
Loop #5
Test script is complete
[1]+ Done ./test5.sh
           

当脚本test5.sh运行在后台模式时,我们输入了命令ls myprog*。脚本输出、输入的命令以及命令输出全都混在了一起。真是让人头昏脑胀!最好是将后台运行的脚本的STDOUT和STDERR进行重定向,避免这种杂乱的输出。

运行多个后台作业

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

$ ./test6.sh &
[1] 3568
$ This is Test Script #1
$ ./test7.sh &
[2] 3570
$ This is Test Script #2
$ ./test8.sh &
[3] 3573
$ And...another Test script
$ ./test9.sh &
[4] 3576
$ Then...there was one more test script
           

每次启动新作业时, Linux系统都会为其分配一个新的作业号和PID。通过ps命令,可以看到所有脚本处于运行状态。

$ ps
PID TTY TIME CMD
2431 pts/0 00:00:00 bash
3568 pts/0 00:00:00 test6.sh
3570 pts/0 00:00:00 test7.sh
3573 pts/0 00:00:00 test8.sh
3574 pts/0 00:00:00 sleep
3575 pts/0 00:00:00 sleep
3576 pts/0 00:00:00 test9.sh
3577 pts/0 00:00:00 sleep
3578 pts/0 00:00:00 sleep
3579 pts/0 00:00:00 ps
           

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

说明 本章之前曾经提到过当你要退出终端会话时,要是存在被停止的进程,会出现警告信息。但如果使用了后台进程,只有某些终端仿真器会在你退出终端会话前提醒你还有后台作业在运行。

如果希望运行在后台模式的脚本在登出控制台后能够继续运行,需要借助于别的手段。

在非控制台下运行脚本

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

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

nohup命令的格式如下:

$ nohup ./test1.sh &
[1] 3856
$ nohup: ignoring input and appending output to 'nohup.out'
           

和普通后台进程一样, shell会给命令分配一个作业号, Linux系统会为其分配一个PID号。区别在于,当你使用nohup命令时,如果关闭该会话,脚本会忽略终端会话发过来的SIGHUP信号。

由于nohup命令会解除终端与进程的关联,进程也就不再同STDOUT和STDERR联系在一起。为了保存该命令产生的输出, nohup命令会自动将STDOUT和STDERR的消息重定向到一个名为nohup.out的文件中。

说明 如果使用nohup运行了另一个命令,该命令的输出会被追加到已有的nohup.out文件中。当运行位于同一个目录中的多个命令时一定要当心,因为所有的输出都会被发送到同一个nohup.out文件中,结果会让人摸不清头脑。

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

$ 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
           

输出会出现在nohup.out文件中,就跟进程在命令行下运行时一样。

作业控制

在本章的前面部分,你已经知道了如何用组合键停止shell中正在运行的作业。在作业停止后,Linux系统会让你选择是终止还是重启。你可以用kill命令终止该进程。要重启停止的进程需要向其发送一个SIGCONT信号。

启动、停止、终止以及恢复作业的这些功能统称为作业控制。通过作业控制,就能完全控制shell环境中所有进程的运行方式了。本节将介绍用于查看和控制在shell中运行的作业的命令。

查看作业

作业控制中的关键命令是jobs命令。 jobs命令允许查看shell当前正在处理的作业。

$ cat test10.sh
#!/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..."
#
           

脚本用$$变量来显示Linux系统分配给该脚本的PID,然后进入循环,每次迭代都休眠10秒。可以从命令行中启动脚本,然后使用Ctrl+Z组合键来停止脚本。

$ ./test10.sh
Script Process ID: 1897
Loop #1
Loop #2
^Z
[1]+ Stopped ./test10.sh
           

还是使用同样的脚本,利用&将另外一个作业作为后台进程启动。出于简化的目的,脚本的输出被重定向到文件中,避免出现在屏幕上。

$ ./test10.sh > test10.out &
[2] 1917
           

jobs命令可以查看分配给shell的作业。 jobs命令会显示这两个已停止/运行中的作业,以及它们的作业号和作业中使用的命令。

$ jobs
[1]+ Stopped ./test10.sh
[2]- Running ./test10.sh > test10.out &
           

要想查看作业的PID,可以在jobs命令中加入-l选项(小写的L)。

$ jobs -l
[1]+ 1897 Stopped ./test10.sh
[2]- 1917 Running ./test10.sh > test10.out &
           

jobs命令使用一些不同的命令行参数,见表16-2。

表16-2 jobs命令参数

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

你可能注意到了jobs命令输出中的加号和减号。带加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。

当前的默认作业完成处理后,带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少个正在运行的作业。

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

$ ./test10.sh > test10a.out &
[1] 1950
$ ./test10.sh > test10b.out &
[2] 1952
$ ./test10.sh > test10c.out &
[3] 1955
$
$ jobs -l
[1] 1950 Running ./test10.sh > test10a.out &
[2]- 1952 Running ./test10.sh > test10b.out &
[3]+ 1955 Running ./test10.sh > test10c.out &
           

我们调用了kill命令向默认进程发送了一个SIGHUP信号,终止了该作业。在接下来的jobs命令输出中,先前带有减号的作业成了现在的默认作业,减号也变成了加号。

$ kill 1955
$
[3]+ Terminated ./test10.sh > test10c.out
$ jobs -l
[1]- 1950 Running ./test10.sh > test10a.out &
[2]+ 1952 Running ./test10.sh > test10b.out &
$
$ kill 1952
$
[2]+ Terminated ./test10.sh > test10b.out
$
$ jobs -l
[1]+ 1950 Running ./test10.sh > test10a.out &
           

尽管将一个后台作业更改为默认进程很有趣,但这并不意味着有用。下一节,你将学习在不

用PID或作业号的情况下,使用命令和默认进程交互。

重启停止的作业

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

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

$ ./test11.sh
^Z
[1]+ Stopped ./test11.sh
$
$ bg
[1]+ ./test11.sh &
$
$ jobs
[1]+ Running ./test11.sh &
           

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

$ ./test11.sh
^Z
[1]+ Stopped ./test11.sh
$
$ ./test12.sh
^Z
[2]+ Stopped ./test12.sh
$
$ bg 2
[2]+ ./test12.sh &
$
$ jobs
[1]+ Stopped ./test11.sh
[2]- Running ./test12.sh &
           

命令bg 2用于将第二个作业置于后台模式。注意,当使用jobs命令时,它列出了作业及其状态,即便是默认作业当前并未处于后台模式。要以前台模式重启作业,可用带有作业号的fg命令。

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

由于作业是以前台模式运行的,直到该作业完成后,命令行界面的提示符才会出现。

调整谦让度

在多任务操作系统中( Linux就是),内核负责将CPU时间分配给系统上运行的每个进程。 调度优先级( scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)。在Linux系统中,由shell启动的所有进程的调度优先级默认都是相同的。

调度优先级是个整数值,从-20(最高优先级)到+19(最低优先级)。默认情况下, bash shell以优先级0来启动所有进程。

窍门 最低值20是最高优先级,而最高值19是最低优先级,这太容易记混了。只要记住那句俗语“好人难做”就行了。越是“好”或高的值,获得CPU时间的机会越低。

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

nice 命令

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

$ nice -n 10 ./test4.sh > test4.out &
[1] 4973
$
$ ps -p 4973 -o pid,ppid,ni,cmd
PID PPID NI CMD
4973 4721 10 /bin/bash ./test4.sh
           

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

nice命令会让脚本以更低的优先级运行。但如果想提高某个命令的优先级,你可能会吃惊。

$ nice -n -10 ./test4.sh > test4.out &
[1] 4985
$ nice: cannot set niceness: Permission denied
[1]+ Done nice -n -10 ./test4.sh > test4.out
           

nice命令阻止普通系统用户来提高命令的优先级。注意,指定的作业的确运行了,但是试图使用nice命令提高其优先级的操作却失败了。

nice命令的-n选项并不是必须的,只需要在破折号后面跟上优先级就行了。

$ nice -10 ./test4.sh > test4.out &
[1] 4993
$
$ ps -p 4993 -o pid,ppid,ni,cmd
PID PPID NI CMD
4993 4721 10 /bin/bash ./test4.sh
           

renice 命令

有时你想改变系统上已运行命令的优先级。这正是renice命令可以做到的。它允许你指定运行进程的PID来改变它的优先级。

$ ./test11.sh &
[1] 5055
$
$ ps -p 5055 -o pid,ppid,ni,cmd
PID PPID NI CMD
5055 4721 0 /bin/bash ./test11.sh
$
$ renice -n 10 -p 5055
5055: old priority 0, new priority 10
$
$ ps -p 5055 -o pid,ppid,ni,cmd
PID PPID NI CMD
5055 4721 10 /bin/bash ./test11.sh
           

renice命令会自动更新当前运行进程的调度优先级。和nice命令一样, renice命令也有一些限制:

 只能对属于你的进程执行renice;

 只能通过renice降低进程的优先级;

 root用户可以通过renice来任意调整进程的优先级。

如果想完全控制运行进程,必须以root账户身份登录或使用sudo命令。

定时运行作业

当你开始使用脚本时,可能会想要在某个预设时间运行脚本,这通常是在你不在场的时候。

Linux系统提供了多个在预选时间运行脚本的方法: at命令和cron表。每个方法都使用不同的技术来安排脚本的运行时间和频率。

用 at 命令来计划执行作业

at命令允许指定Linux系统何时运行脚本。 at命令会将作业提交到队列中,指定shell何时运行该作业。 at的守护进程atd会以后台模式运行,检查作业队列来运行作业。大多数Linux发行版会在启动时运行此守护进程。

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

后面几节会介绍如何用at命令提交要运行的作业以及如何管理这些作业。

at命令的格式

at命令的基本格式非常简单:

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

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

在如何指定时间这个问题上,你可以非常灵活。 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,加不加年份均可。

 你也可以指定时间增量。

 当前时间+25 min

 明天10:15 PM

 10:15+7天

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

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

作业队列的字母排序越高,作业运行的优先级就越低(更高的nice值)。默认情况下, at的作业会被提交到a作业队列。 如果想以更高优先级运行作业, 可以用-q参数指定不同的队列字母。

获取作业的输出

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

这里有个在CentOS发行版中使用at命令安排作业执行的例子。

$ cat test13.sh
#!/bin/bash
# Test using at command
#
echo "This script ran at $(date +%B%d,%T)"
echo
sleep 5
echo "This is the script's end..."
#
$ at -f test13.sh now
job 7 at 2015-07-14 12:38
           

at命令会显示分配给作业的作业号以及为作业安排的运行时间。 -f选项指明使用哪个脚本文件, now指示at命令立刻执行该脚本。

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

$ cat test13b.sh
#!/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
#
$
$ at -M -f test13b.sh now
job 8 at 2015-07-14 12:48
$
$ cat test13b.out
This script ran at July14,12:48:18
This is the script's end...
           

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

列出等待的作业

atq命令可以查看系统中有哪些作业在等待。

$ at -M -f test13b.sh teatime
job 17 at 2015-07-14 16:00
$
$ at -M -f test13b.sh tomorrow
job 18 at 2015-07-15 13:03
$
$ at -M -f test13b.sh 13:30
job 19 at 2015-07-14 13:30
$
$ at -M -f test13b.sh now
job 20 at 2015-07-14 13:03
$
$ atq
20 2015-07-14 13:03 = Christine
18 2015-07-15 13:03 a Christine
17 2015-07-14 16:00 a Christine
19 2015-07-14 13:30 a Christine
           

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

删除作业

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

$ atq
18 2015-07-15 13:03 a Christine
17 2015-07-14 16:00 a Christine
19 2015-07-14 13:30 a Christine
$
$ atrm 18
$
$ atq
17 2015-07-14 16:00 a Christine
19 2015-07-14 13:30 a Christine
           

只要指定想要删除的作业号就行了。只能删除你提交的作业,不能删除其他人的。

安排需要定期执行的脚本

用at命令在预设时间安排脚本执行非常好用,但如果你需要脚本在每天的同一时间运行或是每周一次、每月一次呢?用不着再使用at不断提交作业了,你可以利用Linux系统的另一个功能。

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

cron时间表

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

cron时间表允许你用特定值、取值范围(比如1~5)或者是通配符(星号)来指定条目。例如,如果想在每天的10:15运行一个命令,可以用cron时间表条目:

在dayofmonth、 month以及dayofweek字段中使用了通配符,表明cron会在每个月每天的10:15执行该命令。要指定在每周一4:15 PM运行的命令,可以用下面的条目:

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

这里还有另外一个例子:在每个月的第一天中午12点执行命令。可以用下面的格式:

dayofmonth表项指定月份中的日期值( 1~31)。

说明 聪明的读者可能会问如何设置一个在每个月的最后一天执行的命令,因为你无法设置dayofmonth的值来涵盖所有的月份。这个问题困扰着Linux和Unix程序员,也激发了不少解决办法。常用的方法是加一条使用date命令的if-then语句来检查明天的日期是不是01: 00 12 * * * if [

date +%d -d tomorrow

= 01 ] ; then ; command 它会在每天中午12点来检查是不是当月的最后一天,如果是, cron将会运行该命令。

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

15 10 * * * /home/rich/test4.sh > test4out
           

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

构建cron时间表

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

$ crontab -l
no crontab for rich
           

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

浏览cron目录

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

$ ls /etc/cron.*ly
/etc/cron.daily:
cups makewhatis.cron prelink tmpwatch
logrotate mlocate.cron readahead.cron
/etc/cron.hourly:
0anacron
/etc/cron.monthly:
readahead-monthly.cron
/etc/cron.weekly:
           

因此,如果脚本需要每天运行一次,只要将脚本复制到daily目录, cron就会每天执行它。

anacron程序

cron程序的唯一问题是它假定Linux系统是7× 24小时运行的。除非将Linux当成服务器环境来运行,否则此假设未必成立。

如果某个作业在cron时间表中安排运行的时间已到,但这时候Linux系统处于关机状态,那么这个作业就不会被运行。当系统开机时, cron程序不会再去运行那些错过的作业。要解决这个问题,许多Linux发行版还包含了anacron程序。

如果anacron知道某个作业错过了执行时间,它会尽快运行该作业。这意味着如果Linux系统关机了几天,当它再次开机时,原定在关机期间运行的作业会自动运行。

这个功能常用于进行常规日志维护的脚本。如果系统在脚本应该运行的时间刚好关机,日志文件就不会被整理,可能会变很大。通过anacron,至少可以保证系统每次启动时整理日志文件。

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

$ sudo cat /var/spool/anacron/cron.monthly
20150626
           

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

$ sudo cat /etc/anacrontab
# /etc/anacrontab: configuration file for anacron
# See anacron(8) and anacrontab(5) for details.
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22
#period in days delay in minutes job-identifier command
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly
           

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

period条目定义了作业多久运行一次,以天为单位。 anacron程序用此条目来检查作业的时间戳文件。 delay条目会指定系统启动后anacron程序需要等待多少分钟再开始运行错过的脚本。

command条目包含了run-parts程序和一个cron脚本目录名。 run-parts程序负责运行目录中传给它的任何脚本。

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

identifier条目是一种特别的非空字符串,如cron-weekly。它用于唯一标识日志消息和错误邮件中的作业。

使用新 shell 启动脚本

如果每次运行脚本的时候都能够启动一个新的bash shell(即便只是某个用户启动了一个bash shell),将会非常的方便。有时候,你希望为shell会话设置某些shell功能,或者只是为了确保已经设置了某个文件。

回想一下当用户登入bash shell时需要运行的启动文件(参见第6章)。另外别忘了,不是所有的发行版中都包含这些启动文件。基本上,依照下列顺序所找到的第一个文件会被运行,其余的文件会被忽略:

 $HOME/.bash_profile

 $HOME/.bash_login

 $HOME/.profile

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

每次启动一个新shell时, bash shell都会运行.bashrc文件。可以这样来验证:在主目录下的.bashrc文件中加入一条简单的echo语句,然后启动一个新shell。

$ cat .bashrc
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# User specific aliases and functions
echo "I'm in a new shell!"
$
$ bash
I'm in a new shell!
$
$ exit
exit
           

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

继续阅读