Cron 表达式怎么读:日与星期是「或」,以及下一次到底什么时候跑
一行 crontab 只有五个字段,但它每年制造的意外排得进运维工单前列:该跑的没跑、不该跑的跑了一天、“每周五 13 号"写不出来。原因基本都在三件事上——段的顺序、日和星期的合并规则、以及"下一次"是对着哪台时钟算的。
五段的顺序就是读的顺序
分钟 小时 日 月份 星期
0-59 0-23 1-31 1-12 0-7 (0 和 7 都是周日)
第一段是分钟,不是小时——绝大多数"我以为每天凌晨一点跑,结果它每天跑 60 次"都错在这。每段可以写单值、列表 1,15,30、范围 1-5、步长 */10 或 0-59/5;月份和星期还认三字母名称 JAN…DEC、SUN…SAT。剩下的合法内容只有别名(@daily、@weekly、@hourly、@reboot)和笔误。
6 段是前面多一列秒,7 段是末尾多一列年。这两种在 Vixie cron 和 Linux /etc/crontab 里都不存在,是 Quartz / Spring @Scheduled / EventBridge 的方言;同一天又出现 L(当月最后一天)、W(最邻近工作日)、#(第三个周五写 5#3),各家语义并不一致。先确认你面对的是哪种方言,再去解释这一行。
一、日和星期同时限制时是「或」
这是 cron 最反直觉的一条:当两段的日期列都做了限制,只要任一段匹配就执行,不是交集。
0 0 13 * 5 # 每月 13 号,外加每个周五
按直觉它像"恰好落在周五的 13 号”,实测排出来的下一次是 2026-10-09(周五)、10-13(周二)、10-16(周五)、10-23、10-30、11-06、11-13(这天既是 13 号也是周五)——两类的并集。往后数 366 天:52 个周五 + 12 个 13 号 − 2 天重叠 = 62 次,而不是你以为的"大概一年两次"。
反过来,只要其中一段是 *(或 ?),就完全由另一段决定。于是有个纯逻辑的推论:0 0 13 * 0-6 里星期段被写成 0–6(等于整周,仍算"有限制"),两段并起来覆盖每一天,它就变成每天跑。而"周五 13 号"这种需求写不进一行 crontab,只能 0 0 * * 5 触发后在命令体里自己 if [ "$(date +\%d)" = 13 ] 判一句。
二、*/5 是从零点对齐,不是"距上次五小时"
小时段写 */5 得到的是 00:00、05:00、10:00、15:00、20:00——步长是拿当前时钟和字段各段比对算出来的。cron 不记得上一次是什么时候跑的,所以任务跑慢了、跳过了一次,它也不会补,更不会有"上次成功"的概念。要"每 5 小时一次"这种相对节奏,得靠 systemd timer 的 OnUnitActiveSec= 或外部调度器。
同一个原因让 0 0 30 * * 表现出"每月最后一天不是 30 号就不跑"的怪样子:它匹配"每个月的第 30 天",2 月根本没有这天,所以 2 月直接缺席。想要"每月最后一天",5 段 cron 没有通用写法(L 是各家扩展),常见妥协是 0 0 28-31 * * 加一句"明天是不是 1 号"的判断。
三、0 0 29 2 * 下一次要等四年
按这台机器的日历从 2026-10-04 往后排:0 0 29 2 * 的下一次是 2028-02-29,再下一次是 2032-02-29。中间的 2029、2030、2031 都不存在 2 月 29 日——而如果表达式再刁钻一点,要撞上 2100 年,那也不是闰年(能被 100 整除但不能被 400 整除),2100 年 2 月只有 28 天,这一天要等到 2104。所以任何"下一次"的表都要写明它按哪个日历、哪个时区排的。想核对"那个日子离今天还剩几天",用日期间隔计算器算一遍,别手算跨闰年的那段。
四、夏令时那一小时:跳过是日历决定的,跑几次是实现决定的
用 ICU 日历实测(不是查文档,是数同一分钟能对上几个瞬间):
- America/New_York,2026-03-08(周日)本地 02:30 对应 0 个瞬间——时钟从 01:59 直接跳到 03:00。所以
30 2 * * *在那一天没有任何匹配的时刻,往下一天。 - 同地 2026-11-01(周日)本地 01:30 对应 2 个瞬间(05:30Z 与 06:30Z)——秋天回调让这一小时出现两次。
- Asia/Shanghai 两个探测点都只有 1 个瞬间:中国自 1991 年后不再实行夏令时,北京常年 GMT+8,所以挂在北京时间服务器上的
30 2 * * *一年 365 次(闰年 366),不存在"跳过"或"重复"这一类问题。
日历只告诉你"这一小时不存在"或"存在两次";你的 cron 那天到底跑 0 次、1 次还是 2 次,是实现自己的策略,Vixie / cronie 的 crontab(5) 各写各的,权威在那台机器的手册页,不在任何网页或在线解析器。跨时区团队约好的"每天凌晨两点报表",如果那台机器在夏令时区,就必然有几天不准——这类需求应该写成 UTC 时刻,或者用带时区字段的调度器(cronie 这类实现支持在 crontab 里写 CRON_TZ=,Debian 自带的 cron 手册里没有它,别假设)。
五、“这一行没错但没执行"的排查清单
按命中概率排:
- 时区。 服务器时钟和你不是同一个(“说明明还有 3 小时,它已经跑过了"最常见就是这个)。先
date看服务器,再对照表达式。 - CRLF。 用 Windows 编辑器改过的 crontab 每行末尾带
\r,轻则命令里多个看不见的字符,重则整行被拒绝。sed -i 's/\r$//'或dos2unix过一遍。 %没转义。 在 Vixie/cronie 这一系里,命令里的%会被换成换行、第一个之后的内容变成命令的标准输入——写日期格式date +%F必须写成date +\%F。这一条几乎人人踩一次。- PATH 与相对路径。 cron 给你的环境是一个极简的 shell,
~展开、PATH里的用户目录都不一定在。命令写绝对路径,脚本里先cd。 - 没有装载。
crontab -e或crontab file才生效;直接编辑/var/spool/cron/...里的文件在多数实现上不会自动装载,/etc/cron.d/则要求文件里没有:和.(含扩展名)且字段多一列用户名。 - 命令本身失败了。 任何解析器都看不出这个:把输出重定向到日志(
>> /var/log/job.log 2>&1)再判断,邮件通知在多数精简系统上根本没配。
六、什么时候别用 cron
需要"错过就补跑"的用 systemd timer(Persistent=true)或外部调度器;需要"第 N 个工作日"“距上次 X 秒"的也一样——cron 的模型只有"当前时钟与各段比对”。跨机器的任务依赖(A 完再跑 B)不该靠时间对齐来实现。
顺手用在线 cron 解析对一遍:它把每一段展开成真正匹配的取值,明说这行按 5 段还是 6 段读,并用你这台机器的日历排出接下来 8 次实际时刻(连同用的时区、以及"这个表达式在短月份不匹配"的提示)。厂商扩展(L/W/#、步长 0)它只报那个读不懂的 token,不发明含义——这也是你的 crontab 该有的态度。
相关问题
0 0 * * 0 和 0 0 * * 7 一样吗? 一样,0 和 7 都是周日。@weekly 到底是哪一天? 各家实现是周日,但别名不是标准,别赌。
表达式会被传到别处吗? 不会。解析就是页面脚本里的字符串切分,时刻来自浏览器自己的日历,没有哪一步经过服务器,你输入的表达式不会离开这台设备。
为什么"每天 00:00"和"每小时第 0 分"看起来一样但次数不同? 0 0 * * * 一天一次,0 * * * * 一天 24 次——第一段是分钟,第二段是小时,把第二段写成 * 就从"0 点"变成"每小时”。