1 搞清概念
1.1 cron与crond
cron 是Linux下实现任务调度(定时任务)的一种服务,可以在无需人工干预的情况下运行作业。
crond 则是 cron 服务的守护进程,与Windows下的计划任务类似。
Linux系统会默认安装cron服务工具,并自动启动crond进程。
1.2 crontab
crontab 是Linux系统提供的用于设置定时任务的命令行工具。
crontab 也指 cron 服务的配置文件,是“cron table”的缩写,语义上即为“任务调度列表”。
1.3 系统任务调度与用户任务调度
Linux的任务调度主要分为两类:
系统任务调度:系统周期性所要执行的工作,比如系统数据备份、临时文件清理、缓存清理等。
用户任务调度:某个用户定期要执行的工作,比如用户数据备份、定时邮件提醒等,这些工作可由每个用户自行设置。
2 搞清权限
默认情况下,只有root可以使用crontab,而普通用户不可以,会出现如下提示信息:
root可以通过/etc/cron.allow
与/etc/cron.deny
两个文件来控制哪个用户有权使用crontab。这两个文件的内容与格式很简单,将需要配置的用户名写入文件,每个用户名占一行。
权限规则如下:
cron.allow | cron.deny | 有权使用crontab的用户范围 | |
1 | 文件不存在 | 文件不存在 | 仅root可使用crontab |
2 | 文件存在 | 文件不存在 | 仅cron.allow中的用户可使用crontab |
3 | 文件不存在 | 文件存在 | 只要不在cron.deny中的用户都可以使用crontab 特殊情况:cron.deny文件内容为空,则所有用户都可以使用crontab |
4 | 文件存在 | 文件存在 | 仅cron.allow中的用户可使用crontab(cron.allow优先级高于cron.deny,此时cron.deny不起作用) |
由于cron.allow
与cron.deny
同时存在的情况下,cron.deny
没有实际意义。因此从逻辑、语义清晰的角度出发,不建议同时保留两个文件。根据实际需求,白名单与黑名单保留一个就好。
3 搞清配置文件
系统任务调度对应的配置文件有/etc/crontab
文件和/etc/cron.d/
目录下的一系列文件。
用户任务调度对应的配置文件在/var/spool/cron/
目录下。
cron服务每隔1分钟会去读取这些文件中的调度任务。
这一个小节我们先从宏观上认识这些配置文件,而这些文件的详细配置语法我们留到下一个小节讨论。
3.1 /etc/crontab
/etc/crontab
是一个文件,root可以通过这个文件来维护系统级的调度任务。
一般情况下,不建议将所有的全局性的调度任务都维护在/etc/crontab
这一个文件中,那样会很混乱,因此有了/etc/cron.d/
。
3.2 /etc/cron.d/
/etc/cron.d/
是一个目录,也由root维护。这个目录下可以存放多个crontab任务文件,这样我们就可以以文件的粒度对不同类别的任务调度进行管理了。
/etc/cron.d/
目录下的任务调度文件需要遵循以下命名规范才能被cron服务正确扫描:
- 只能包含字母、数字、下划线和中划线(尤其注意不能包含
.
)
/etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly、/etc/cron.monthly 这些是什么?
系统预设了这四个目录,用来放置每小时、每天、每周、每月要执行的可执行脚本文件。
注意:这四个目录只是预设,并没有完全启用,并不是直接将可执行脚本文件放入其中就可以,还需要run-parts
命令的配合。
笔者所使用的CentOS 7.2中,就只有 /etc/cron.hourly 这一个目录作为系统样例被启用了(配置在了/etc/cron.d/
目录下的 0hourly 文件中)。
3.3 /var/spool/cron/
/var/spool/cron/
是一个目录,用来存放包括root在内的每个用户独有的crontab任务文件,文件名同用户名,如用户“pcserver10”的任务文件为“/var/spool/cron/pcserver10”。
/var/spool/cron/
目录下的crontab任务文件不可以直接创建或直接修改,而是通过crontab
命令来维护。
4 搞清语法
4.1 cron服务启停
功能 | 命令 |
查看cron服务状态 | service crond status |
启动cron服务 | service crond start |
停止cron服务 | service crond stop |
重启cron服务 | service crond restart |
调度任务没有执行,我们首先要通过service crond status
来确认cron服务是否已启动。
在调度任务突然失效的情况下,我们可以尝试service crond restart
重启cron服务来解决问题。
4.2 crontab命令
crontab
命令用来维护用户级的调度任务,其实就是维护/var/spool/cron/
目录下对应用户的crontab任务文件,语法格式如下:
crontab [-u user] file
crontab [-u user] { -e | -l | -r }
参数说明:
- -u user:用来指定要维护哪一个用户的crontab任务,此选项一般由root用户来使用,普通用户维护自己的crontab任务表时不需要此选项。
- file:file是文件名,我们可以提前维护好一个任务文件,然后通过
crontab file
命令将所指定的文件内容载入到crontab中。 - -e:进入文本编辑器,编辑当前或指定用户的任务信息(本质上是在编辑一个临时文件,这个临时文件先读取了
/var/spool/cron/
目录下对应用户的crontab任务文件的内容,完成编辑后,保存并退出文本编辑器的时候,会用临时文件的内容覆盖对应的crontab任务文件)。 - -l:显示当前或指定用户的任务信息。
- -r:删除当前或指定用户的全部任务信息(即删除了
/var/spool/cron/
目录下对应用户的crontab任务文件)。
特别注意:
由于crontab -r
删除任务表后不可恢复,因此强烈建议为任务表做好备份。
也就我们在维护任务表时,不要因为方便而使用crontab -e
直接编辑任务表,而是应该将任务表的内容维护至一个备份文件中,再通过crontab file
命令更新任务表,这样我们就能留有一份备份文件,以防误操作使用了crontab -r
而无法恢复。
4.3 crontab文件配置 - 调度任务部分
不管是/etc/crontab
文件本身、/etc/cron.d/
目录下的任务文件,还是通过crontab
命令维护的/var/spool/cron/
目录下的任务文件,这些文件的配置语法基本一致(仅/var/spool/cron/
下的文件稍微有不同)。
以/etc/crontab
为例,我们来看一下crontab任务文件的内容,可以将其分为环境变量和调度任务两个部分:
4.3.1 cron任务的基本格式
每一个cron任务的格式如下:
<分钟> <小时> <日期> <月份> <星期> [<用户>] <指令>
- 前五个配置项用来表示任务执行的时间。
- 【用户】用来指定执行任务的用户,仅适用于系统级的任务调度文件。(通过
crontab
命令来维护的用户级任务调度(/var/spool/cron/
目录下的任务文件)不存在此配置项 ) - 【指令】就是要执行的具体任务。
4.3.2 时间的配置
首先来看各个时间配置项的取值范围:
配置项 | 取值范围 |
分钟 | 取值范围:0-59 |
小时 | 取值范围:0-23 |
日期 | 取值范围:1-31 |
月份 | 取值范围:1-12,也可以使用 |
星期 | 取值范围:0-7(0与7均代表星期日),也可以使用 |
除了上述的取值范围外,还支持以下特殊符号:
特殊符号 | 详细说明 |
*(星号) | “每一个”的意思,代表着取值范围内的任何一个有效值都需要执行指令。 |
-(减号) | 用来表示时间范围(只支持数字,不支持用名称来表示的月份与星期),如:“3-6” |
,(逗号) | 可以用来分隔开一系列的值,来形成一个时间列表(只支持数字,不支持用名称来表示的月份与星期),如:“1,3,5,7”、“0-4,8-12” |
/n(斜线 + 数字) | 用来指定时间间隔与频率,也就是“每隔 n 个时间单位”的意思,如:“*/2”用在小时上代表着每2个小时、“0-29/2”用在分钟上代表着前半个小时中每2分钟 |
下面是一些例子:
表达式 | 说明 |
0 6 * * * | 每天早6点 |
0 */2 * * * | 每两个小时 |
0 23-7/2,8 * * * | 每晚11点到早8点间的每两个小时以及早8点 |
0 11 * * 1-3 | 每周一到周三的早11点 |
0 4 1 1 * | 1月1日的早4点 |
5,15,25,35,45,55 16,17,18 * * * | 每天下午4点、5点、6点的5分、15分、25分、35分、45分、55分 |
特别注意:
可以分别以周或以日月为单位,但不要使用 “几月几号且为星期几” 的模式,如:“0 6 1 1 3”。从语法上这个时间表达式并没有错误,可以运行。但是从语义上却存在歧义,我们可能希望1月1号且为周三时执行,但系统有可能会处理为每年1月1号与每个周三分别执行。
我们还可以用以下“昵称”来替代五个时间配置项:
昵称 | 详细说明 |
@reboot | 系统重启时执行一次指令 |
@yearly | 每年1月1号0点0分执行一次指令,即“0 0 1 1 *” |
@annually | 同@yearly |
@monthly | 每月1号0点0分执行一次指令,即“0 0 1 * *” |
@weekly | 每周日0点0分执行一次指令,即“0 0 * * 0” |
@daily | 每天0点0分执行一次指令,即“0 0 * * *” |
@hourly | 每小时0分执行一次指令,即“0 * * * *” |
4.3.3 指令的形式
指令可以有以下几种形式:
- 单条命令,如:
* * * * * echo -e $(date '+\%Y\%m\%d') >> /root/tmp.log
- 以
;
分隔的多条命令,如:* * * * * . /etc/profile;/bin/sh /root/scripts/test.sh
- 完整的可执行脚本路径,如:
* * * * * /root/scripts/test.sh
- 通过
run-parts
命令来执行某个目录下的所有可执行脚本文件,如:* * * * * run-parts /root/scripts/cron.minitely
注意:%
是crontab的特殊字符,第一个%
后的所有数据都会被作为标准输入。在指令中如需使用%
,则需要进行转义\%
,如:date '+\%Y\%m\%d'
。
4.3.4 如何实现秒级的任务?
crontab可配置的最小的时间粒度是分钟,那么我们如何实现秒级的任务呢?有如下两种方式:
- 相同的任务配置多条,通过
sleep
来制造执行的时间差,如每10秒执行一次:
* * * * * command
* * * * * sleep 10; command
* * * * * sleep 20; command
* * * * * sleep 30; command
* * * * * sleep 40; command
* * * * * sleep 50; command
- 仅配置一条任务,在任务所执行的脚本内,通过
for
循环实现每隔n秒执行一次的逻辑,如:
* * * * * script
#!/bin/sh
STEP=5
for ((i=0; i < 60; i+=STEP))
do
TIME_NOW=$(date "+%Y-%m-%d %H:%M:%S")
echo -e $TIME_NOW >> /root/scripts/logs/test.log
sleep $STEP
done
4.4 crontab文件配置 - 环境变量部分
4.4.1 基本说明
在crontab任务文件的头部,可以设置以下环境变量:
环境变量 | 说明 | 由crond设置的默认值 |
SHELL | 指定系统要使用哪种shell解释器 | /bin/sh |
PATH | 指定系统执行命令的路径 | /usr/bin:/bin |
MAILTO | 指定cron将任务执行的相关信息通过电子邮件发送给哪个用户 | 无 |
HOME | 指定执行命令或者脚本时使用的主目录 | 根据crontab的拥有者从 |
4.4.2 PATH 环境变量不起作用的巨坑
当我们的指令或脚本依赖于环境变量的时候,问题就出现了。在命令行中可以顺利运行的指令或脚本,在crontab中却无法正确执行。这是因为crontab不会从用户的profile文件中读取环境变量参数。
这时我们就会把目光转向PATH
,直接设置这个变量不就可以了嘛。然而,这个变量似乎就是个摆设,并不会按照我们想象的那样去运行。
这里记录一下PATH
的实验过程:笔者的CentOS 7.2,系统默认安装了OpenJDK,版本为号为1.8.0.181。笔者又重新配置了1.8.0_144版本的JDK,写入了/etc/profile
系统环境变量中:java版本验证如下:
现在我们写一个定时任务,将
java -version
的输出结果写入日志中:
* * * * * java -version 2>> /root/scripts/logs/test_java.log
看一下结果:
这不是我们想要的结果,很明显环境变量出了问题,我们把环境变量打印出来:
* * * * * echo -e "PATH="$PATH >> /root/scripts/logs/test_java.log;java -version 2>> /root/scripts/logs/test_java.log
再看结果:
环境变量果然不对,当前是crontab文档中说的由crond所设置的默认值。我们通过
PATH
手动修改一下吧:
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/java/jdk1.8.0_144/bin:/usr/java/jdk1.8.0_144/jre/bin:* * * * * echo -e "PATH="$PATH >> /root/scripts/logs/test_java.log;java -version 2>> /root/scripts/logs/test_java.log
再看结果:
环境变量倒是设置成功了,但是实际上并没有生效,所以,设置
PATH
感觉就是设置了个寂寞。
那么环境变量的问题该如何解决呢?有以下两种方式:
- 修改cron任务,在原有的指令前增加读取环境变量文件的命令:
* * * * * . /etc/profile; your command
- 在所要执行的脚本文件的开头增加读取所需的环境变量文件的命令:
#!/bin/sh
. /etc/profile
. ~/.bash_profile
4.4.3 crontab执行后发送的邮件
每一个调度任务执行完毕时,都会将标准输出与错误输出信息以电子邮件的形式发送给对应的用户,/var/spool/mail/
目录下的与用户同名的文件用来存放对应用户所接收到的邮件信息,邮件内容如:
我们要讨论的第一个问题就是MAILTO
所能起到的作用。
(1)在没有配置MAILTO
的情况下:
/etc/crontab
文件和/etc/cron.d/
目录下的任务文件,配置调度任务时会指定执行用户,电子邮件相应的也就会发送给这个用户。- 通过
crontab
命令维护的任务文件自身属于哪个用户,电子邮件就发送给哪个用户。
(2)配置了MAILTO
的情况下:
MAILTO
可以改变上述的默认行为,将电子邮件发送给指定的用户。MAILTO
作用于文件中的所有调度任务,且仅在当前文件中生效,不会跨文件生效。- 仅定义
MAILTO
但不赋值(即MAILTO=
),此时不会发送任何邮件。
调度任务执行后发送邮件,这样日积月累,邮件文件会越来越大,严重的情况下可能会影响系统的运行。我们要讨论的下一个问题就是妥善的处理邮件信息。
上面已经提到了,我们可以通过定义一个空的MAILTO
,来禁止发送邮件的行为。但MAILTO
会作用于文件中所有的任务,是否有办法从单个任务的角度出发来解决这个问题呢?我们可以使用输出重定向,在每条指定后增加: >/dev/null 2>&1
。(当前,这么做的前提是,对所需要的正常的输出已经做了处理,比如追加到某个日志文件中。)
4.4.4 体验 HOME 的作用
虽然实际情况下,基本用不到也不建议使用HOME
(建议所有的地方都使用绝对路径,以屏蔽因路径而引发的各类问题),但并不妨碍我们实践并体验一下HOME
的作用。
我们在/root/scripts
目录下做4个测试脚本,并将HOME
的值设置为此目录,然后以4种不同的写法做测试:
测试结果如下:
sh home_test_01
,因为已指定了HOME=/root/scripts
,执行脚本时会到此路径下去寻找脚本文件,因此能够正确执行。sh $HOME/home_test_02
和$HOME/home_test_04
,在指令中引用了所设置的环境变量HOME
,实际上相当于指明了绝对路径,可以正确执行。home_test_03
不能执行,不通过命令(sh
)执行的脚本文件,只有写完整的路径才能执行。
上述测试过程仅为体验,依然建议:所有的地方都使用绝对路径!!!
4.5 其他注意事项
- 应该为调度任务编写注释,以
#
开头的行即为注释行。 - root用户可以查看cron服务的运行日志,
/var/log/cron
。