cron:把”每天凌晨三点”写进系统里

本文介绍 cron 的模型与用法:crontab 五段式语法、用户级与系统级任务的区别,以及环境变量、输出、时区这三个最常见的坑,顺带聊聊它与 systemd timer 的取舍。

cron 是什么

cron 是 Unix 世界的定时任务守护进程,1970 年代中期诞生于 Bell Labs,名字取自希腊语 chronos(时间)。它的工作模型简单到近乎朴素:一个常驻后台的 daemon 每分钟醒一次,翻一遍任务表,看看有没有到点的活儿,有就 fork 出 shell 去执行。五十多年过去,这个模型基本没变——变的只是实现细节,今天 Linux 发行版上跑的多半是 cronie 或 vixie-cron 的后代,但 crontab 那五行魔法依然原样可用。

任务表本身叫 crontab(cron table)。用户级任务用 crontab -e 编辑、crontab -l 查看,实际存放在 /var/spool/cron/ 下;系统级任务则直接写在 /etc/crontab/etc/cron.d/ 里的文件中,每行多一个执行用户的字段,便于以指定身份运行。此外 /etc/cron.daily/etc/cron.weekly/etc/cron.monthly 三个目录里的脚本会被按日、周、月自动执行,放一个可执行文件进去就算登记完成。

五段式语法

一行任务就是”什么时间 + 干什么”。时间部分是五个字段:

字段取值范围常用写法
分钟0–5930*/5
0–2339-18
1–31*
1–12*
0–7(0 和 7 都是周日)1-5

* 表示任意,*/5 是步进,9-18 是区间,逗号可以并列。最容易反直觉的一条是:日和周同时受限时是”或”而不是”与”——0 0 1 * 1 表示每月 1 号每个周一的零点,不是两者的交集。需要交集语义时只能让命令自己 test 判断。

# 每天凌晨 3:30 备份
30 3 * * * /usr/local/bin/backup.sh
 
# 工作日 9-18 点每五分钟巡检,输出落日志
*/5 9-18 * * 1-5 /usr/local/bin/check.sh >> /var/log/check.log 2>&1

另有一组 @ 开头的快捷记号:@daily@hourly@reboot 等,语义一目了然。@reboot 尤其实用——开机后跑一次的初始化活儿,不用再想怎么算开机时间。

三个经典坑

第一个坑是环境。cron 执行任务时的环境干净得可怜:PATH 通常只有 /usr/bin:/bin,SHELL 是 /bin/sh,没有 tty,HOME 也未必是你登录时的那个。于是”终端里跑得好好的,挂到 cron 就哑火”成了经典悬疑剧。纪律很简单:脚本里的命令一律绝对路径,或开头自己 export PATH;依赖 bash 特性的话给脚本加上 shebang,别赌默认 shell。

第二个坑是输出。任务有 stdout/stderr 而你不处理时,cron 会试图给用户发本地邮件——没装 MTA 的机器上这些输出静默消失,报错信息一起消失。要么重定向到日志文件,要么在 crontab 里设 MAILTO="" 明确丢弃,别让错误死在黑洞里。

第三个坑是时区。cron 按系统本地时间触发,而容器和云主机默认 UTC 的居多,你以为的”凌晨三点”实际是北京时间上午十一点;夏令时切换的凌晨还会出现被跳过或重复的小时。定时逻辑对时区敏感时,先 timedatectl 确认,别信直觉。

还有一个语法级暗桩:crontab 里的 % 是特殊字符,代表换行。命令里出现 date +%F 这类写法必须转义成 \%,否则第一个 % 之后的内容会被截断当作 stdin 喂给命令,行为极其诡异。

重入与替代品

cron 对任务本身一无所知:上一轮没跑完,下一轮照样拉起。这就是 flock 里详细讨论过的重入问题,最省事的解法是拿 flock -n 包一层,跳过而非堆积。想确认某条任务到底跑没跑,grep CRON /var/log/syslogjournalctl -t CROND 能看到 daemon 的调度记录,比盯着结果瞎猜可靠得多。

新一些的选择是 systemd timer:.timer.service 成对定义,天然防重叠、日志自动进 journal、还支持机器关机错过后的追赶执行。代价是要写两份 unit 文件,语法啰嗦不少。我的经验法则:一两条简单定时任务,cron + flock 又快又稳;任务一多、需要日志和依赖编排,或者本来就在 systemd 体系里,就上 timer。

工具能活五十年,在软件圈是稀罕事。cron 的长寿恰恰说明:每分钟醒一次看一眼表,对绝大多数定时需求来说,已经足够。