Linux cron 定时任务
从 crontab 表达式到排错实践的完整指南
cron 是类 Unix 系统中基于时间的任务调度器,守护进程 crond(在 Debian/Ubuntu 上叫 cron)每分钟扫描一次 crontab 表,匹配到当前时间就触发对应命令。它的用途非常广泛:定期备份、日志轮转、证书续期、数据同步、健康检查……几乎所有"每隔一段时间就要跑一遍"的需求都可以交给它。这篇文章梳理 crontab 的表达式语法、常用命令、系统级目录约定,以及生产环境里最容易踩到的坑。
表达式语法
一条 crontab 规则由五个时间字段加上一个命令字段组成,字段之间用空格或制表符分隔:
┌──────────── minute (0 - 59)
│ ┌────────── hour (0 - 23)
│ │ ┌──────── day of month (1 - 31)
│ │ │ ┌────── month (1 - 12) 或 jan,feb,mar,apr ...
│ │ │ │ ┌──── day of week (0 - 6, 0=Sunday) 或 sun,mon ...
│ │ │ │ │
* * * * * command原文里写了一个
[optional] seconds字段,那是 some 实现(如ISC Cron 之外的 Quartz、Spring@Scheduled、croniter等)才有的扩展,标准 Vixie cron / cronie / Debian cron 只支持 5 个字段,后面示例均以 5 字段为准。
四个特殊字符在所有字段中都通用:
*—— 代表"从最小值到最大值"的所有取值,即不做限制。-—— 闭区间,例如8-11表示 8、9、10、11。,—— 列表,例如1,2,5,9,也可以混用区间0-4,8-12。/—— 步长,例如*/2表示每 2 个单位,0-23/2表示从 0 点起每 2 小时。
需要注意 day of month 与 day of week 的逻辑是 OR 而不是 AND:当两个字段都"被限制"(即都不是 *)时,任一字段匹配都会触发。Debian crontab(5) 给的例子是 30 4 1,15 * 5,它的含义是"每月 1 号、15 号 4:30,以及每个周五 4:30",而不是"既是 1/15 号又是周五"。如果想要 AND 语义,得在命令里再判断一次日期。
命名取值用月份和星期英文前三个字母(大小写不敏感),如 jan、sun,但不能用区间或列表形式(1-5 没问题,mon-fri 不行)。0 和 7 都代表周日。
@ 别名快捷写法
man page 提供了一组替代五字段的"昵称",使用时以 @ 开头,只占一个字段位置:
| 别名 | 等价表达式 | 含义 |
|---|---|---|
@reboot | (无等价) | 系统启动后运行一次 |
@yearly / @annually | 0 0 1 1 * | 每年 1 月 1 日 0 点 |
@monthly | 0 0 1 * * | 每月 1 日 0 点 |
@weekly | 0 0 * * 0 | 每周日 0 点 |
@daily | 0 0 * * * | 每天 0 点 |
@hourly | 0 * * * * | 每小时整点 |
@reboot 在容器/云主机里尤其方便,可以在机器拉起时初始化工作目录、注册服务。
在线表达式生成器
手写表达式容易把"每 5 分钟"和"第 5 分钟"写反,推荐用 crontab-generator 在线生成并预览下一次执行时间,再贴回 crontab。
常用命令
crontab(1) 是用户管理自己调度表的入口,常用选项:
crontab -e # 编辑当前用户的 crontab(用 $EDITOR 指定的编辑器)
crontab -l # 打印当前用户的 crontab 到 stdout
crontab -l > cron.bak # 备份当前 crontab
crontab cron.txt # 用 cron.txt 文件整体替换当前 crontab
crontab -r # 删除当前用户的 crontab(谨慎!)
crontab -ri # 删除前询问 y/N 确认
crontab -u app -l # 以 root 身份查看指定用户的 crontabcrontab -l | crontab - 这种管道写法非常常用——- 表示从 stdin 读取并替换。注意 Debian 系默认在 -l 时会去掉头部那三行 DO NOT EDIT THIS FILE 注释,因此这个管道是幂等的;如想保留头部,把环境变量 CRONTAB_NOHEADER 设为 N。
用户的 crontab 实际保存在 /var/spool/cron/crontabs/ 下,文件名就是用户名。该目录只有 crontab 组可写,不要手工编辑这些文件——只能通过 crontab -e 修改,这样 cron 才会重新加载并校验。
系统级定时任务
除了每个用户一份的 crontab,系统层面还有几处约定的位置:
/etc/crontab—— 系统主 crontab,格式比用户 crontab 多一列用户名,例如30 3 * * * root /usr/local/bin/backup.sh。在 Debian/Ubuntu 上,它默认通过run-parts调用下面四个目录。/etc/cron.d/—— drop-in 目录,适合软件包或运维脚本分发定时任务,文件格式同/etc/crontab(含用户名),独立于/etc/crontab,不继承主 crontab 的环境变量。文件名只能由字母、数字、下划线、连字符组成,带点会被忽略。/etc/cron.{hourly,daily,weekly,monthly}/—— 把可执行脚本放进去即可被run-parts周期性调用,无需关心表达式。脚本必须可执行、属主为 root、对 group/other 不可写。/etc/cron.allow//etc/cron.deny—— 访问控制白/黑名单,二者都存在时以allow为准;都缺失时,在一些发行版上仅 root 能用crontab。
如果机器装了 anacron,Debian 会禁用 /etc/crontab 里的 daily/weekly/monthly 行,改由 anacron 处理,以支持"关机错过了也能补跑";hourly 仍由 cron 直接触发。
常见问题
下面这些坑几乎每个写过 crontab 的人都至少踩过一次,逐一过一遍。
1. 默认 shell 是 /bin/sh 而不是 bash
这是最容易出问题的一点。crontab(5) 明确写着:SHELL 默认设置为 /bin/sh。在 Debian/Ubuntu 上 /bin/sh 是 dash,一个 POSIX 兼容但不支持 bash 扩展的轻量 shell。因此下面这些写法在 cron 里会失败:
[[ ... ]]双括号测试- 数组
arr=(a b c)、arr[@] function name { ... }关键字source file(.才是 POSIX)- 进程替换
<(cmd) $RANDOM、$SECONDS等 bash 内置变量- bash 风格的字符串展开
${var,,}、${var/foo/bar/}
解决方式:在 crontab 顶部显式切换 shell,并把 PATH 写全:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * /opt/scripts/sync.sh或者把命令包成 bash -l -c '...',以登录 shell 方式运行(会读取 /etc/profile、~/.bash_profile)。参考 Linux crontab 输出重定向不生效问题解决 的分析。
2. 文件包含 Windows 换行(CRLF)
从 Windows 编辑器拷过来的 cron.txt 经常带 \r\n,crontab 解析时会把 \r 当成时间字段的一部分,报出 bad minute / bad command 之类的怪错,任务"看起来存在却永远不执行"。处理方法:
dos2unix cron.txt
# 或者
sed -i 's/\r$//' cron.txtvi/vim 里可以用 :set fileformat=unix 然后保存。其它常见原因可参考 为什么 crontab 不执行。
3. PATH 极简,命令找不到
cron 给出的环境非常"干净",通常只有 PATH=/usr/bin:/bin(甚至完全没设 PATH,继承自 cron 守护进程的启动环境)。因此 node、docker、go、/usr/local/bin/ 下的程序、自己装的二进制,都可能报 command not found。最稳妥的做法是命令一律用绝对路径:
*/10 * * * * /usr/local/bin/python3 /opt/app/healthcheck.py >> /var/log/hc.log 2>&1环境变量也建议显式声明在 crontab 头部(name = value 形式),不要指望从 ~/.bashrc 继承——cron 不会读取它。
4. 百分号 % 被当作换行
在命令字段里,未转义的 % 会被翻译成换行,且第一个 % 之后的内容作为 stdin 传给命令。这是 date 命令最常见的陷阱:
# 错误:%Y 会被截断
0 0 * * * echo "today is $(date +%Y-%m-%d)" >> /tmp/d.log
# 正确:转义反斜杠(或在脚本里调用 date)
0 0 * * * echo "today is $(date +\%Y-\%m-\%d)" >> /tmp/d.log把逻辑放进 shell 脚本里再用 cron 调用,可以彻底规避这类字符问题。
5. 没有重定向输出,失败时毫无痕迹
crontab(5) 规定:只要 MAILTO 没有置空,命令的 stdout/stderr 会被邮寄给 crontab 所有者(或 MAILTO 指定的地址)。现代服务器通常没配 MTA,邮件静默失败,结果就是"任务没跑也不知道"。所以务必显式重定向:
MAILTO=""
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1把 MAILTO="" 显式置空,既能让 cron 不去发邮件,又能避免邮件堆积在队列里。
6. 任务重叠运行
cron 本身没有锁,如果上一次任务还没跑完,下一次触发会再启动一个实例,可能造成资源争抢或重复写入。用 flock 加文件锁可以优雅解决:
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /opt/scripts/sync.sh-n 表示拿不到锁就立即退出,不会排队等待。
7. 排错时怎么看日志
确认任务是否被触发,看 cron 守护进程日志:
# systemd 系统
journalctl -u cron --since "1 hour ago"
journalctl -u crond -f # CentOS/RHEL,实时跟踪
# 老式 rsyslog
grep CRON /var/log/syslog日志里出现 CMD (...) 行就说明 cron 已经触发命令了;如果命令没产生预期效果,问题大概率在脚本环境(参照上面 1/3/4 条)。注意 Ubuntu 24.04 最小安装默认不带 rsyslog,/var/log/syslog 不存在,只能用 journalctl。
实用示例
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
# 每天凌晨 3 点全量备份,带文件锁防重叠
0 3 * * * /usr/bin/flock -n /tmp/backup.lock /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# 每 5 分钟拉取一次健康指标
*/5 * * * * /opt/scripts/healthcheck.py
# 每周日凌晨 4 点清理 7 天前的日志
0 4 * * 0 /usr/bin/find /var/log/app -type f -mtime +7 -delete
# 每月 1 号 0 点跑月报
0 0 1 * * /opt/scripts/monthly_report.sh
# 工作日 9:30 发站内信提醒
30 9 * * 1-5 /usr/local/bin/notify "standup"
# 开机后延迟初始化
@reboot /opt/scripts/bootstrap.sh小结
cron 是个朴素却长青的工具,80% 的"踩坑"其实来自两点:运行环境与交互 shell 不一致、表达式语义有偏差。把 SHELL / PATH / MAILTO 在 crontab 顶部写清楚,命令一律用绝对路径,任务加上输出重定向和 flock 锁,再配合 journalctl -u cron 排查,绝大多数定时任务都能稳定跑起来。