在 Linux 中,我们经常使用 cron 执行一些定时任务,只需要配置一下时间,它就可以周期的执行一些任务。
不知道你是否清楚它的详细用法?是否发现,脚本单独运行时是好好的,放到 cron 任务里却挂了!!!一个部署了 crond 的服务器,系统资源却被莫名其妙的被占满了,Why???
简介
#
在 Linux 平台上如果需要实现定时或者周期执行某一个任务,可以通过编写 cron 脚本来实现。Linux 默认会在开机时启动 crond 进程,该进程负责读取调度任务并执行,用户只需要将相应的调度脚本写入 cron 的调度配置文件中。
另外,需要注意的是,cron 采用的是分钟级的调度,如果要更高精度的,只能用其它方法。
而且,
每次执行时都是并发执行
,而非串行。
安装、启动
#
在很多的发行版本中,cron 是默认安装的,包括了 crond+crontab 两个主要命令。前者是后台任务,用于调度执行;后者用来编译、查看、管理定时任务。
在 CentOS 7 中,该服务是默认安装的,如果没有,则可以安装 cronie 包,然后通过如下命令启动 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指令,在孙子进程中执行