Linux下Crontab使用问题集
2015-11-06 18:34:35 阿炯

-----------------------------------------------------
crontab 与 crond


crontab是Unix和Linux用于设置需要周期性被执行的指令,是Linux服务器很常用的技术,很多任务都会设置在crontab循环执行,如果不使用crontab,那么任务就是常驻程序,这对程序或脚本的缩写要求比较高,一个要求程序是7X24小时不宕机,另外是要求调度程序比较可靠,实际工作中,大部分的的程序都没有必要花这么多时间和精力去解决上面的两个问题的,只需要写好自己的业务逻辑,通过crond这个工业级程序去调度就行了。

crond:控制周期性任务计划调度的后台进程,daemon。

crontab:提供给用户控制任务计划的命令,创建、删除、编辑任务计划等。该命令从标准输入设备读取指令,并将其存放于“crontab”文件中(/var/spool/cron/以用户命名的文件),以供之后读取和执行。

每一个用户都可以有自己的crontab文件,但在一个用户较多的系统中,我们通常会限制。为了安全问题,可以限制能够使用 crontab 的用户:/etc/cron.allow,  /etc/cron.deny

当用户使用crontab命令新建任务计划之后,该项 jobs 就会被 /var/spool/cron/ 目录下,而且以用户账号来创建一个文件,每一项任务计划为一行。

crond 检测的时间周期是 “分钟”, 每分钟会读取一次 /etc/crontab,以及 /var/spool/cron 里面的记录并执行。crond 执行的每一项任务计划,都会被记录到 /var/log/cron 这个日志文件。

如果碰到没有按预定的时间计划执行的任务(脚本应用本身没有问题,手动执行能完成),最应用排查的方面是其运行的环境变量或其中的路径可能是相对的导致的问题。


更多关于环境变量的设置,请参考:bash shell 环境设置参考

-----------------------------------------------------
Debian下Crontab取消邮件发送


debian下的定时执行任务在完成或失败后都会给该任务属主发送一封邮件,即使将该脚本的所有输出重定向到null设备(>/dev/null 2>&1);相当多的时候会发给最多登录的用户。在用户终端使用时,会时不时地提醒有新的邮件,但实际上并不需要,如何才能取消呢。

解决办法:在该项crontab任务前加'MAILTO=""'提示符,如下:
* * * * * MAILTO="" myscript

-----------------------------------------------------
解决Debian的Crontab执行时间时差问题


在定时任务中会发现显示的是UTC的时间,但系统是CST的时间。可见在Debian里crontab的执行时间与我们date看到的还是有时差。虽然可在处理的候算上时差(8小时),但多少有些不便,像日志切分时就会多或少一些。Debian里设置时间由两部分组成,localtime和timezone。平时直接用tzselect设置了时区后似乎能看到的时间都是正确的,也就忽略了localtime。但实际上crontab的执行时间是受localtime决定的,所以还需要修改这个文件:
rm -fv /etc/localtime
cp -fv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

有时不能如愿,可改一下系统的时区设置(/etc/timezone):
dpkg-reconfigure tzdata

或使用tzselect指令来设置时区信息。

然后重启cron(这步不可少)
service cron restart

-----------------------------------------------------
crontab中的输出配置


crontab中经常配置运行脚本输出为:>/dev/null 2>&1,来避免crontab运行中有内容输出。

shell命令的结果可以通过‘> ’的形式来定义输出

/dev/null 代表空设备文件  

> 代表重定向到哪里,例如:echo "freeoa" > /home/freeoa.txt 

1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于"1>/dev/null"

2 表示stderr标准错误

& 表示等同于的意思,2>&1,表示2的输出重定向等同于1 

那么重定向输出语句的含义:

1>/dev/null 首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,不显示任何信息。

2>&1 表示标准错误输出重定向等同于标准输出,因为之前标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。

更多关于重定向,请参考:Linux重定向命令应用及语法

-----------------------------------------------------
Crontab内环境变量与Shell环境变量的问题处理办法


crontab有一个问题,就是它总是不会缺省的从用户profile文件中读取环境变量参数,经常导致在手工执行某个脚本时是成功的,但是到crontab中试图让它定期执行时就是会出错。

两种方法可以解决这个问题

1、在Shell文件里面获取环境变量值的路径写成绝对路径,别用环境变量的路径值。例如获取CPU的使用情况  通过绝对路径/proc/cpuinfo 来获取值;

2、Shell脚本缺省的#!/bin/sh开头换行后的第一行用
#!/bin/sh
. /etc/profile
. ~/.bash_profile

这样,crontab在执行脚本的时候,就能够读到用户的环境变量参数。

备注:如果你是在cron里提交的,请注意:不要假定cron知道所需要的特殊环境,它其实并不知道。所以你要保证在shell脚本中提供所有必要的路径和环境变量,除了一些自动设置的全局变量。

脚本里面文件路径没有写全路径导致的,另一次是因为脚本运行需要依赖java环境变量,其实两次都是环境变量的问题造成的。从网上同样了解到一般crontab无法运行的问题都是由环境变量在crontab中不一定可识别引起的。

要保证在shelll脚本中提供所有必要的路径和环境变量,除了一些自动设置的全局变量。所以注意如下3点:
1)脚本中涉及文件路径时写全局路径;

2)脚本执行要用到java或其他环境变量时,通过source命令引入环境变量,如:
cat freeoa_cbp.sh
#!/bin/sh
source /etc/profile
export RUN_CONF=/home/freeoa/conf/platform/app.conf

3)当手动执行脚本OK,但是crontab死活不执行时。这时必须怀疑是环境变量的问题,并可以尝试在crontab中直接引入环境变量解决问题。如:
0 * * * * . /etc/profile;/path_of_script

其他应该注意的问题
1)新创建的cron job,不会马上执行,至少要过2分钟才执行,如果重启cron则马上执行。
2)每条 JOB 执行完毕之后,系统会自动将输出发送邮件给当前系统用户。日积月累会非常的多,甚至会撑爆整个系统。所以每条 JOB 命令后面进行重定向处理是非常必要的:>/dev/null 2>&1 。前提是对 Job 中的命令需要正常输出已经作了一定的处理, 比如追加到某个特定日志文件。
3)当crontab突然失效时,可以尝试/etc/init.d/crond restart解决问题。或者查看日志看某个job有没有执行/报错tail -f /var/log/cron。
4)千万别乱运行crontab -r。它从Crontab目录(/var/spool/cron)中删除用户的Crontab文件。删除了该用户的所有crontab都没了。
5)在crontab中%是有特殊含义的,表示换行的意思。如果要用的话必须进行转义\%,如经常用的date '+%Y%m%d'在crontab里是不会执行的,应该换成date '+\%Y\%m\%d'。
6)工作中遇到比较多的是root密码过期,导致它的cronjob无法正常执行。
7)周与日月不可同时并存,要么指定周, 要么指定日月。

-----------------------------------------------------
使用非交互式的方式编辑crontab任务

crontab任务的结构语法

* * * * * "command to be executed"
- - - - -
| | | | |
| | | | ----- Day of week (0 - 7) (Sunday=0 or 7)
| | | ------- Month (1 - 12)
| | --------- Day of month (1 - 31)
| ----------- Hour (0 - 23)
------------- Minute (0 - 59)

方式一:将已有的任务导出到文件,然后在文件中加入新的任务后再导入
#将已有的任务导出
crontab -l > mycron
#在已有文件中追加入新的任务
echo "00 09 * * 1-5 echo 'Hello FreeOA'" >> mycron
#导入文件以完成新的任务的添加
crontab mycron && rm -fv mycron

方法二:直接输出后在线编辑后在导入

crontab -l | { cat; echo "0 0 0 0 0 some entry"; } | crontab -

crontab -l lists the current crontab jobs, cat prints it, echo prints the new command and crontab - adds all the printed stuff into the crontab file. You can see the effect by doing a new crontab -l.

方法三:使用grep移出指定的相关任务

croncmd="/home/freeoa/myfun args >/dev/null 2>&1"
cronjob="0 */15 * * * $croncmd"

加入到crontab,不带重复:
(crontab -l | grep -v -F "$croncmd" ; echo "$cronjob") | crontab -

从现有的任务列表中移除:
(crontab -l | grep -v -F "$croncmd") | crontab -

注意:grep的'-F'或'--fixed-regexp' 将范本样式视为固定字符串的列表。

以下代码同上:
command="php /scripts/gitcron.php"
job="0 0 * * 0 $command"
cat <(fgrep -i -v "$command" <(crontab -l)) <(echo "$job") | crontab -

cat <(crontab -l) <(echo "1 2 3 4 5 script.sh") | crontab -

(crontab -l | grep . ; echo -e "0 4 * * * myscript\n") | crontab -

-----------------------------------------------------
crontab功能对某用户不可用(权限或语法问题)

主机上普通用户执行crontab -e|-l 时,会报错,信息如下:
You (username) are not allowed to access to (crontab) because of pam configuration.

问题排查思路:
一、检查crond权限。
1)more /etc/corn.deny,文件是空的。
2)ls -lh /usr/bin/crontab,具备S权限位,正常。

二、检查PAM模块。
more /etc/pam.d/crond,文件配置正常,与其他主机上的无异。

三、查看系统日志
more /var/log/secure,日志中显示有用户密码过期!
(user) FAILED to authorize user with PAM (Permission denied)
pam_unix(crond:account): expired password for user username (password aged)
使用passwd命令修改一下用户的密码,或者,执行'chage –M -1 username'设定用户"密码失效过期时间"从不。

另外可能在安全的配置文件中有错误的设置
/etc/security/access.conf

All users will be denied if the following line has been uncommented:
- : ALL : ALL

You can either change this line to:
+ : ALL : ALL
or add a specific line for the user above the '- : ALL : ALL' line:
+ : username : cron crond

用户没有执行crond的权限
编辑/etc/cron.deny文件来控制哪些用户不能执行crond服务的功能。可以将自己从文件中删去,或者联系root。

没有使用绝对路径
这里的绝对路径包括脚本中的路径和crond命令中的路径两个方面:
1).去邮件( /var/spool/mail/USER)看看,在这个过程中用户应该会收到邮件,去看看里面就有crond的内容,可以截取最后1000行查看。
2).在脚本里面加入output用来调试,可对比和终端下执行脚本的echo $PATH。

如果遇到shell语法错误:Syntax error: "(" unexpected  

解决方法:需指定shell解释器命令:SHELL=/bin/bash请参见上面 crontab编辑示例 SHELL=/bin/bash

或者参见: LINUX - BASH Syntax Error

-----------------------------------------------------
解决cron定时任务产生的多个sh,sendmail僵尸进程

通过top指令查看发现很多个zombie,首先查询是哪个进程引起的:
ps -ef|grep defunct

查询发现僵尸进程是sh,sendmail之类,然后根据父进程的id查一下是哪个程序引起的:
ps -ef|grep 父进程id

发现是cron引起的,即这些进程的父进程都是cron,也就是定时任务程序crontab,然后根据处理以往的僵尸进程的办法,会发现kill不掉僵尸进程,也可能kill不掉僵尸进程的父进程:
ps -ef|grep defunct|grep -v grep|awk '{print $3}'|xargs kill -9

注意:要杀父进程,子进程是杀不掉的。

通过pstree指令可以发现原来是crond在执行脚本时会调用sendmail将脚本输出信息以邮件的形式发送给crond用户,可能后面还会调用postdrop处理邮件。

ps aux | grep -w Z   #用于得到返回僵尸进程的信息,主要是pid
ps o ppid #返回其父id

kill -9 $(ps -A -ostat,pid | awk '/[zZ]/{ print $2 }')
kill -9 $(ps -A -ostat,ppid | grep -e '[zZ]'| awk '{ print $2 }')
kill -9 $(ps -A -ostat,ppid | awk '/[zZ]/ && !a[$2]++ {print $2}')


解决办法:

1、在crontab里面对应的脚本任务后面加上
> /dev/null 2>&1

把标准输出重定向到空设备就可以了。

2、将本地邮件用户置空
将/etc/crontab和/etc/cron.d/*里的MAILTO=root修改为
MAILTO=""

也可在执行'crontab -e'出来的第一行增加一段
MAILTO=""

重启服务后就可以了,排查僵尸进程常用命令:
ps -ef|grep defu
ldd,lsof,strace,pstree

-----------------------------------------------------
执行顺序中的";" 和 "&&" 区别

";" 和 "&&"是有区别的

";":不管cmd1执行的结果如何,都执行cmd2
"&&":只有cmd1执行返回的结果是成功的,才执行cmd2

cmd1 && cmd2; cmd3

- cmd1 is executed, if it succeeds, then execute cmd2. and then cmd3 (regardless of cmd2 success or not)
- cmd1 is executed, if it fails, then cmd3 (cmd2 won't be executed)

-----------------------------------------------------
执行周期内任务未完成导致crond进程太多

全部杀死重启crond服务,使用root执行重启后问题解决,可使用flock来防止脚本周期内未执行完重复执行。
$ flock -xn my.lock cmd

my.lock是一个文件,可以是任意文件,可以新建一个空文件,当flock 获得锁后就会执行后面的 cmd。

测试过程:
T1: flock -xn my.lock sleep 20
T2: flock -xn my.lock ls

只有当T1返回后,T2的ls才会成功。如果某脚本要运行30分钟,可以在Crontab里把脚本间隔设为至少一小时来避免冲突。而比较糟的情况是可能该脚本在执行周期内没有完成,接着第二个脚本又开始运行了。如何确保只有一个脚本实例运行呢?一个好用的方法是利用lockfFreeBSD 8.1下为lockf,CentOS  5.5下为flock,在脚本执行前先检测能否获取某个文件锁,以防止脚本运行冲突。

lockf 参数如下:
-k:一直等待获取文件锁。
-s:silent,不发出任何信息,即使拿不到文件锁。
-t seconds:设定timeout的时间是seconds秒,如果超过时间,则自动放弃。

以下crontab计划任务执行前,需获取临时文件create.lock 文件锁,crontab计划任务的内容如下:
1 */10 * * * * (lockf -s -t 0 /tmp/create.lock /usr/bin/perl /home/freeoa/bin/createtab.pl  >> /home/freeoa/logs/create.log 2>&1)

若第一个实例在10分钟内没有运行完,第2个实例不会运行。以前是通过Shell脚本来解决这个问题的,比如用while...do循环,然后放在后台执行;但后来发现其实用flock或lockf方法更为简单。

附上linux下的flock的用法:
flock (util-linux 2.13-pre7)
Usage: flock [-sxun][-w #] fd#
flock [-sxon][-w #] file [-c] command...

-s  --shared     Get a shared lock
#共享锁,在定向为某文件的FD上设置共享锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置独占锁的请求失败,而其他进程试图在定向为此文件的FD上设置共享锁的请求会成功

-x  --exclusive  Get an exclusive lock
#独占或排他锁,在定向为某文件的FD上设置独占锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置共享锁或独占锁都会失败。只要未设置-s参数,此参数默认被设置

-u  --unlock     Remove a lock
#手动解锁,一般情况不必须,当FD关闭时,系统会自动解锁,此参数用于脚本命令一部分需要异步执行,一部分可以同步执行的情况

-n  --nonblock   Fail rather than wait
#为非阻塞模式,当试图设置锁失败,采用非阻塞模式,直接返回1,

-w  --timeout    Wait for a limited amount of time
#设置阻塞超时,当超过设置的秒数,就跳出阻塞,返回1

-o  --close      Close file descriptor before running command
-c  --command    Run a single command string through the shell 执行其后的comand
-h  --help       Display this text
-V  --version    Display version

脚本执行示例:
每天23:30的时候执行一个脚本,但是执行前必须要获得排他文件锁,否则无法执行命令:
1 30 23 * * * flock -xn /tmp/test.lock -c 'perl /path/freeoate.pl'


-----------------------------------------------------


-----------------------------------------------------


-----------------------------------------------------

-----------------------------------------------------


-----------------------------------------------------

该文章最后由 阿炯 于 2021-02-21 17:18:05 更新,目前是第 2 版。