在 Linux 中,我们经常使用 cron 执行一些定时任务,只需要配置一下时间,它就可以周期的执行一些任务。
不知道你是否清楚它的详细用法?是否发现,脚本单独运行时是好好的,放到 cron 任务里却挂了!!!一个部署了 crond 的服务器,系统资源却被莫名其妙的被占满了,Why???
简介
在 Linux 平台上如果需要实现定时或者周期执行某一个任务,可以通过编写 cron 脚本来实现。Linux 默认会在开机时启动 crond 进程,该进程负责读取调度任务并执行,用户只需要将相应的调度脚本写入 cron 的调度配置文件中。
另外,需要注意的是,cron 采用的是分钟级的调度,如果要更高精度的,只能用其它方法。
而且,每次执行时都是并发执行,而非串行。
安装、启动
在很多的发行版本中,cron 是默认安装的,包括了 crond+crontab 两个主要命令。前者是后台任务,用于调度执行;后者用来编译、查看、管理定时任务。
在 CentOS 7 中,该服务是默认安装的,如果没有,则可以安装 cronie 包,然后通过如下命令启动 crond 服务。
# systemctl start crond
其它的相关操作与 systemctl
指令相同,如 status、restart、stop 等操作。
相关配置文件
与 cron 相关的有如下的几个文件,其中前几个是调度相关的文件:
/etc/crontab
全局配置文件;同时每个用户有自己的 cron 配置文件,这些文件在/var/spool/cron
目录下。/etc/cron.deny
/etc/cron.allow
用来控制可以使用 crontab 命令的用户,如果两个文件同时存在,那么/etc/cron.allow
优先;如果两个文件都不存在,那么只有超级用户可以安排作业。/etc/cron.d/
/etc/{cron.daily,cron.hourly,cron.monthly,cron.weekly}
保存的配置文件,后面详解。/var/log/cron
默认的日志文件。如果配置使用了syslog
,那么日志会发送到/var/log/messages
文件中。
对于 CentOS 7 来说,有 cron.deny
文件,但是为空,而且没有 cron.allow
文件,这也就意味着所有的用户都可以创建 cron 任务。
配置定时任务
定时任务包括了两类:
- 系统级任务
/etc/crontab
需要 root 权限,其中第六部分指定了用户名,也就是说能以任意用户执行命令;通常用于系统服务或者一些重要的任务。 - 用户级任务
/var/spool/cron/
注意,此时的第六部分为用户需要执行的命令,而且只能以创建任务的用户身份执行。
如果用的任务不是以 hourly
monthly
weekly
方式执行,则可以将相应的 crontab
写入到 crontab
或 cron.d
目录中。如,可以在 cron.d
目录下新建脚本 echo-date.sh
。
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
通常来说,我们是通过 crontab 命令来管理定时任务,常用的参数有:
-e
编辑crontab
任务,默认使用当前用户。-l
列出用户所有的定时任务;注意,不会显示/etc
目录下的配置任务。-r
删除所有的定时任务。-u
指定用户名。
最常见的是通过 crontab -e
编辑 crontab 任务,此时会通过环境变量 $EDITOR
定义的值,调用相应的编辑器,例如 export EDITOR="/usr/bin/vim"
,通过 vim 进行编辑。此时会在 /tmp
目录下生成一个临时文件,当编辑完成后替换掉 /var/spool/cron/
中对应用户的文件。
另外,也可以编辑一个临时文件如 contabs.tmp
,编辑后通过 contab contabs.tmp
命令导入新的配置。一般不建议直接修改 /etc/
下的相关配置文件。
通常来说,需要检查 /etc/crontab
文件、/etc/cron.*/
目录,以及 /var/spool/cron/
目录。
Anacron
其实在官方的源码中,还包括了一个 anacron 指令,而 CentOS 7 中是包含在 anaconda-core 包中的;所以,如果使用 anacron 命令,必须安装该包。
简介
像服务器来说,Linux 主机通常是 24 全天全年的处于开机状态,此时只需要 atd 与 crond 这两个服务即可;而对于像我们自己的电脑,经常关机,那么我们就需要 anacron 的帮助了。
anacron 并不能取代 cron,而是以天为单位或者是在启动后立刻进行 anacron 的动作。它会去侦测停机期间该执行但未执行的 crontab 任务,并将该任务运行一遍后,anacron 就会自动停止。
通过 anacron 命令,我们可以选择串行执行(默认)、强制执行(不判断时间戳)、立即执行未执行任务(不延迟等待)等操作。其配置文件是 /etc/anacrontab
,每次执行完成会在 /var/spool/anacron/
目录下保存运行的时间点。
任务配置
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
其中开头定义了一些环境变量等信息,而配置项的含义如下:
period in days
指定指令执行的周期,即指定任务在多少天内执行一次,也可以使用宏定义,如@day
、@weekly
、@monthly
。delay in minutes
当命令已经就绪后,并非立即执行,而是要延迟等待一段时间。job-identifier
每次启动时都会在/var/spool/anacron
里面建立一个以job-identifier
为文件名的文件,记录着任务完成的时间。command
要运行的命令,其中run-parts
用来运行整个目录的可执行程序。
命令详解
实际上,anacron 也是一个 cron 任务,可以通过 ls /etc/cron*/*anacron
查看。通常是通过 anacron -s
执行,以 anacron -s cron.daily
为例,会有如下的步骤:
- 由配置文件
/etc/anacrontab
解析到cron.daily
这项工作名称的执行周期为一天。 - 从
/var/spool/anacron/cron.daily
取出最近一次运行 anacron 的时间戳。 - 把取出的时间戳与当前的时间戳相比较,如果差异超过了一天,那么就准备进行命令。
- 若准备执行命令,根据
/etc/anacrontab
的配置,计算延迟的时间。 - 延迟时间后,开始运行后续命令,也就是
run-parts /etc/cron.daily
这串命令。 - 运行完毕后,anacron 程序结束。
另外,需要注意的是,命令执行通常为串行执行。
示例
在配置 crontab 任务时,有几个特殊的符号:A) *
所有的取值范围内的数字;B) /
每的意思,如 */5
表示每 5 个单位;C) -
从某个数字到某个数字;D) ,
用来分开几个离散的数字。
以下举几个例子说明问题:
*/10 * * * * echo "Ten minutes ago." >> /tmp/foo.txt // 每十分钟执行一次
0 6 * * * echo "Good morning." >> /tmp/foo.txt // 每天早上6点
0 */2 * * * echo "Have a break now." >> /tmp/foo.txt // 每两个小时
45 4 1,10,22 * * echo "Restart server." >> /tmp/foo.txt // 每月1、10、22日的4:45
0 23-7/2,8 * * * echo "Have a good dream." >> /tmp/foo.txt // 晚上11点到早上8点之间每两个小时,早上八点
0 11 4 * 1-3 echo "Just kidding." >> /tmp/foo.txt // 每月4号和每周的周一到周三的早上11点
45 11 * * 0,6 echo "Have a good lunch." >> /tmp/foo.txt // 每周六、周日的11点45分
0 9 * * 1-5 echo "Work hard." >> /tmp/foo.txt // 从周一到周五的9点
2 8-16/3 * * * echo "Some examples." >> /tmp/foo.txt // 8:02、11:02、14:02执行
0,30 18-23 * * * echo "Same." >> /tmp/foo.txt // 每天18:00到23:00之间每隔30分钟
0-59/30 18-23 * * * echo "Same." >> /tmp/foo.txt // 同上
*/30 18-23 * * * echo "Same." >> /tmp/foo.txt // 同上
另外,需要注意的几点:
- 可以在 crontab 的命令中使用环境变量,如:
*/1 * * * * echo $HOME
。 - 第三个域和第五个域是 “或” 操作。
通常,使用 crontab -e
进行的配置是针对某个用户的,而编辑 /etc/crontab
是针对系统的任务。
特殊用法
除了上述的写法外,crontab 提供了一些简单的时间定义方法,如:
@daily echo "Hi" >> /tmp/foo.txt
除此之外还有如下类似的类型,可以通过 man 5 crontab
查看。
@reboot : Run once after reboot.
@yearly : Run once a year, ie. "0 0 1 1 *".
@annually : Run once a year, ie. "0 0 1 1 *".
@monthly : Run once a month, ie. "0 0 1 * *".
@weekly : Run once a week, ie. "0 0 * * 0".
@daily : Run once a day, ie. "0 0 * * *".
@hourly : Run once an hour, ie. "0 * * * *".
常用技巧
可以通过如下命令查看所有用户的 cron 定时任务,注意需要使用 root 权限。
for user in $(cut -f1 -d: /etc/passwd); do echo "### Crontabs for $user ####"; crontab -u $user -l; done
需要注意的是,上述脚本只能显示 user 的 crontabs,如果要查看所有的还需要解析 /etc/crontab
、/etc/crontab.d
、/etc/cron.daily
等配置文件中的任务。
常见问题
简单记录在使用 crontab 时遇到的问题。
% 导致的问题
例如写个了一个 shell 脚本,其中参数中使用了 %
,那么在 cron 任务中就可能会执行错误,或者与预期的不符。为简单起见只是将传入的参数保存在 /tmp/foobar.log
中,脚本文件为 /tmp/foobar.sh
,其内容如下:
#!/bin/bash
echo $1 >> /tmp/foobar.log
然后我们新建一个 cron 任务,内容如下:
*/1 * * * * /tmp/foobar.sh "`date '+%Y-%m-%d %H:%M:%S'`" > /dev/null 2>&1
正常来说在 /tmp/foobar.log
中会每隔 1 分钟打印一条日志,而实际上却没有。查看 /var/log/cron
日志可以发现,是没有完整执行上述命令的。
实际上,在 cron 任务中,%
是有特殊意义的,在这里需要转义,通过 man 5 crontab
查看帮助,可以看到如下内容:
A “%” character in the command, unless escaped with a backslash (\), will be changed into newline characters, and all data after the first % will be sent to the command as standard input.
也就是说,如果 %
没有通过 \
转义,那么就会被替换成换行,上述命令的正确打开姿势是:
*/1 * * * * /tmp/foobar.sh "`date '+\%Y-\%m-\%d \%H:\%M:\%S'`" > /dev/null 2>&1
输出字符引发的血案
现象是,登陆一台公用的跳板机时,当尝试从个人帐号切换到公用帐号时发现报错,是由于进程数超过了最大限制 ulimit -u
,然后通过 ps aux
发现有很多进程,其中比较多的是如下的两条命令:
/usr/sbin/postdrop -r
/usr/sbin/sendmail -FCronDaemon -i -odi -oem -oi -t -f root
基本可以确定是邮件的发送服务导致的,那么是什么程序调用的呢?通过 pstree 发现,其父进程为 crond 。
然后,通过 du -i
查看,发现 /var
的 inode 数目使用了 100%
,通过如下命令查看根目录下的文件数目,也就是大约每个子目录中 inode 的数目。
$ for dir in `ls -1Ad /*`; do echo -e "${dir} \t\t `find ${dir} | wc -l`"; done
然后,可以逐层向下查找,最后可以发现是 /var/spool/postfix/maildrop
目录下有大量的文件。通过 file 命令查看是 data 类型,实际上该目录下的文件可以通过 strings 命令查看,其内容为 crond 发送的邮件。
大概定位到了原因,先恢复掉。kill 掉所有 postdrop、sendmail 进程,然后清空 maildrop 目录下的文件。此时如果直接通过 rm * -f
删除,会由于文件过多而报错,可以通过如下两种方式进行删除。
# find /tmp -type f -exec rm {} \;
# ls | xargs rm -vf
OK,环境已经恢复,那么具体是什么原因导致的呢?
查看 cronie 的源码可以发现,crond 会 fork 一个子进程去执行任务,而该进程有会再 fork 一个孙子进程执行真正的命令,代码的调用逻辑如下。
main() # C入口函数
|-job_runqueue() # 执行队列中的命令
|-do_command() # 此时会fork一个子进程执行,主进程继续工作
|-child_process() # 再fork一个进程,通过execle执行shell命令
|-execle() # 执行真正的shell指令,在孙子进程中执行
在 child_process()
函数中通过 fork()
子进程前,会创建两个管道 (stdin+stdout) 用来与子进程通讯,分别表示输入和输出。默认情况下,crontab 会将命令执行的输出,通过邮件发送给用户。
而在查看 /var/log/maillog
日志发现,是由于 pickup 管道不存在,导致邮件一直在积压。
通常来说,我们不需要发送邮件,为了防止上述问题的发生,可以配置运行脚本输出为 >/dev/null 2>&1
,来避免 crontab 运行中有内容输出。
0 * * * * /path/to/script.sh > /dev/null
0 * * * * /path/to/script.sh > /dev/null 2>&1
0 * * * * /path/to/command arg1 > /dev/null 2>&1 || true
或者直接在 crontab 文件的顶部设置 MAILTO 变量为空,也就是 MAILTO=""
。
当然,如果有必要,可以将输出发送到指定邮箱,但是需要首先确保邮件服务是正常的,然后添加如下配置项:
MAILTO="ooops@foobar.com"
0 3 * * * echo "Helloooo!"
参考
- 源码可以从 cronie project 官方网站下载,相关的帮助可以查看 man 1 crontab (命令行语法相关)、man 5 crontab (配置文件相关)。