crontab 能跑不等于写对了:默认 shell 是 dash、百分号会被吃掉、cron.d 权限不对就静默不跑,这些都在 WSL2 实测过。
一、先分清三类定时任务:用户表、cron.d、cron.daily
crontab -l # 看自己的表;没有时输出 no crontab for root,退出码 1(实测)
crontab -u www-data -l # root 代看别人的表
ls -l /var/spool/cron/crontabs # 用户表落盘位置,实测权限 600、属组 crontab
cat /etc/crontab # 系统表:每行多一列“以哪个用户跑”
三类表的差别不止是放在哪。用户表由 crontab 命令安装进 /var/spool/cron/crontabs,手册明确不建议直接编辑那些文件 [2]。/etc/crontab 与 /etc/cron.d 里的行比用户表多一个用户字段,MAILTO 写在首行可选 [1]。cron.daily、cron.hourly 这些目录不是 cron 直接扫的,而是 /etc/crontab 里一条 run-parts 任务触发:实测那行写作 17 * * * * root cd / && run-parts --report /etc/cron.hourly,也就是每小时第 17 分才跑,不是整点;Debian bookworm 的 cron.daily 定在 04:25 [1]。
谁能用 crontab 还有门槛文件:Ubuntu 24.04 默认 cron.allow 与 cron.deny 都不存在,实测普通用户 nobody 能装自己的表;一旦创建 cron.allow 且只写 root,nobody 立刻被拒,报错原文是 You (nobody) are not allowed to use this program (crontab) [2]。
二、改表的顺序:先备份,再编辑,出错能回滚
crontab -l > /root/cron-backup.txt # 先导出一份,出错好回滚
crontab /root/cron-backup.txt # 回滚 = 用备份文件重装整张表
EDITOR=nano crontab -e # 指定编辑器;VISUAL 优先于 EDITOR(实测)
crontab -ri # 删除整表前要求交互确认
改错了不用慌:校验不过就不安装。实测三种报错原文是 "-":0: bad minute、bad day-of-month、new crontab file is missing newline before EOF, can't install.,退出码都是 1,旧表内容原封不动 [2]。脚本里非交互改表也安全:EDITOR=true crontab -e 打印 No modification made 并返回 0;VISUAL 与 EDITOR 同时设置时 cron 用 VISUAL(实测)。crontab -r 是整表删除而不是清掉一行;加 -i 会先问一句是否真的要删除这张表,但它要真终端——实测把空输入接给它会反复打印 Please enter Y or N 而不退出 [2]。
三、时间字段之外,还有文件名与权限两个静默陷阱
# 用户表:五个时间字段加命令(分 时 日 月 周)
*/15 3-5 * * 1-5 /usr/local/bin/report.sh >> /var/log/report.log 2>&1
# cron.d 里的系统表:多一列用户,文件名别带点
printf '* * * * * appuser /usr/local/bin/heartbeat.sh\n' | sudo tee /etc/cron.d/app-heartbeat
sudo chmod 644 /etc/cron.d/app-heartbeat
五字段取值范围、逗号与斜杠写法、@daily 与 @reboot 这类别名见手册 [1];RHEL、Fedora 用的 cronie 同样支持 @yearly 一类别名,还多支持 CRON_TZ 时区与 RANDOM_DELAY 随机延迟 [3]。两个实测的静默陷阱:文件名带点的 cron.d 文件不执行,hermes.dot 没跑、hermes-dash 跑了,cronie 手册直接要求 cron.d 与用户表的文件名只能含字母、数字、下划线、连字符;权限带组可写位(664)同样被忽略,644 才跑 [3]。这两条都不报错,只表现为“任务凭空不跑”。
四、手动能跑、cron 里失败:多半是 shell、PATH、百分号
readlink /proc/$$/exe # 放进 crontab 跑一次:实测输出 /usr/bin/dash
env | sort # 对照 cron 给任务的环境与你的登录 shell
SHELL=/bin/bash # 写在表顶部,换回 bash
date +\%s # 百分号必须转义,date +%s 写进表里会被截成 date +
Ubuntu 上 cron 默认用 /bin/sh 执行命令,实测那个 sh 就是 dash:表里写双中括号条件式会失败,实测落盘的是 no-bash,表顶部加一行 SHELL=/bin/bash 才走 bash [1]。任务环境也不是你的登录环境:手册说 SHELL 固定为 /bin/sh、HOME 与 LOGNAME 取自 passwd [3],实测任务的 env 与 cron 守护进程自身的 environ 完全一致,PATH 是从 /usr/local/sbin 到 /snap/bin 那一串,跟你 ssh 进去看到的不一样。百分号这条最容易踩:手册写明命令里未转义的百分号会被换成换行符、首个之后的内容变成标准输入 [1],实测日志只留下 (root) CMD (date +),任务静默失败。稳妥做法是命令写绝对路径、输出自己重定向。
五、任务跑没跑、输出去哪了
journalctl 是最省事的入口:journalctl -u cron --since "10 min ago" 实测能看到 (root) CMD (...) 与 pam session 行;Ubuntu 24.04 装了 rsyslog,同一批日志也落 /var/log/syslog,实测那个文件里 CRON 行有 431 条。RPM 系的 crond 走 syslog 还是 journal 由包与 rsyslog 配置决定 [3]。
输出去向看 MAILTO 三态:非空就发到该地址,MAILTO="" 一封不发,不设置则发给表主 [1]。实测对照很直观——设了 MAILTO 为空的那张表没有任何投递尝试;没设的那张在日志里留下 (CRON) info (No MTA installed, discarding output),也就是没装邮件代理时任务输出被直接丢弃。cron.daily、cron.hourly 由 run-parts 执行,想预览会跑哪些脚本可以用 run-parts --list /etc/cron.hourly,实测名字里带点的测试脚本根本不进列表。
六、要补跑或要秒级精度时,换 systemd timer
sudo systemctl list-timers --all # 本机已有 apt-daily、man-db 等计时器(实测)
sudo systemd-analyze calendar 'Mon *-*-* 03:00' # 校验表达式:归一化写法加下次触发(实测)
sudo systemd-analyze calendar '25:00' # 写错直接报 Failed to parse(实测)
timer 与同名 service 配对,Timer 段里 OnCalendar 走挂钟时间,OnBootSec、OnActiveSec、OnUnitActiveSec 走单调时间,后者在虚拟机休眠时也会跟着停 [4]。日历表达式写法与 hourly、daily 这类特殊值见 systemd.time(7) [5]。实测一个 OnActiveSec=1s、OnUnitActiveSec=10s、AccuracySec=1s 的计时器,33 秒内触发 3 次,systemctl list-timers 给出 NEXT、LEFT、LAST、PASSED 四列,排错很直观。文档口径的三条默认值值得记:AccuracySec 默认 1 分钟,不写它秒级计时器也会迟到;RandomizedDelaySec 默认 0;Persistent 默认关闭,开了它才会在开机后补跑错过的任务 [4]。cron 这边对应的是 @reboot,一行就够,但只在 cron 守护进程启动时触发一次 [3]。
| 现象 | 原因 | 处理 | | — | — | — | | 脚本手动跑正常,cron 里失败 | 默认 /bin/sh 是 dash,bash 语法不认 | 表顶部写 SHELL=/bin/bash 或改写成 POSIX 写法(实测) | | 任务像没执行,日志只有一条 CMD | 命令里的百分号未转义,命令被截断 | 百分号前加反斜杠,或改成绝对路径脚本(实测) | | cron.d 文件放了就是不跑 | 文件名带点,或权限带组可写位 | 改名去掉点,chmod 644(实测) | | 找不到任务输出 | 无邮件代理时输出被丢弃 | 自己重定向到文件,或把 MAILTO 指向真实地址(实测) | | 普通用户用不了 crontab | cron.allow 存在且没列该用户 | 把用户名加进 cron.allow(实测) | | cron.daily 比预期晚跑 | 由 /etc/crontab 里的固定分钟触发 | 需要准点就自己写一行 cron.d(实测口径) | | 计时器到点不补跑 | Persistent 默认关闭 | Timer 段加 Persistent=true(文档口径) |
**常见提问**
crontab -l 导出备份,再逐行注释观察一轮,最后才 crontab -ri;实测直接 crontab -r 会清掉整张表。局限性:全部实测在 WSL2 的 Ubuntu 24.04.4(cron 3.0pl1-184ubuntu2、systemd 255.4-1ubuntu8.16)完成;cronie 与 RPM 系的包名、crond 日志去向、@reboot 的真实重启触发、Persistent 补跑、Debian cron.daily 的 04:25 触发点均为文档口径,未在本机复现;容器与独立 systemd 环境下的表现未测。
**实测记录**
(root) CMD (date +) 且无产物,转义后正常。参考资料
- [1] crontab(5) – Ubuntu 24.04 noble manual page(Ubuntu crontab(5))
- [2] crontab(1) – cron – Debian bookworm manual page(Debian crontab(1))
- [3] cronie crontab(5) manual source(cronie 手册)
- [4] systemd.timer(5) – Ubuntu 24.04 noble manual page(systemd.timer(5))
- [5] systemd.time(7) – Ubuntu 24.04 noble manual page(systemd.time(7))
文中主要命令已在 WSL Ubuntu(24.04)实测并核对其在 Debian 系与通用发行版的文档口径。