基本用法
每个用户一份 crontab,里面的命令以该用户的身份执行。常用的操作就四个:
crontab -e
编辑当前用户的 crontab,退出编辑器后修改自动安装。
crontab -l
列出当前用户的 crontab。
crontab -r
删除当前用户的 crontab。删完之后再 crontab -l 会提示 no crontab for root(以 root 为例)。删除之前先备份:crontab -l > crontab.bak。
crontab -n 文件名
试运行:只检查这个文件的语法并输出成功信息,不写入任何 crontab。
crontab 文件实际存放在 /var/spool/cron/crontabs,不应该直接编辑这个目录下的文件,要走 crontab 命令。
在 su 到别的用户的环境里操作时,手册建议始终带上 -u user 明确指定操作哪个用户的 crontab,因为 su 可能让 crontab 搞混当前用户。
权限方面:如果 /etc/cron.allow 存在,只有列在里面的用户能用 crontab;它不存在而 /etc/cron.deny 存在,deny 里列的用户不能用;两个都在时 allow 优先。root 总是允许的。
本文的命令和输出来自 Ubuntu 26.04.1 的 cron 3.0pl1-200ubuntu1 的手册页,以及我在这个系统的容器里(root 用户)的实测。其他发行版可能不一样,拿不准时用 man 5 crontab 核对。
五个时间字段
crontab 里有效的行只有两种:环境变量设置(name = value,等号两边空格可有可无)或者 cron 命令。命令行有五个时间日期字段,字段之间用空格或制表符分隔,命令字段最长 998 个字符。
五个字段从左到右:
分钟 小时 日 月 星期
0-59 0-23 0-31 0-12 0-7
星期字段 0 和 7 都表示星期日。cron 每分钟检查一次,当分钟、小时、月份匹配当前时间,并且“日”与“星期”两个字段中至少有一个匹配时,就执行命令。“至少有一个”这个细节后面单独讲。
写法规则:
*表示“第一个到最后一个”,也就是任意值。- 范围用连字符,两端包含:
8-11表示 8、9、10、11 点。 - 列表用逗号:
1,2,5,9、0-4,8-12。 - 步长用
/,可以跟在范围或星号后面:0-23/2表示每隔一小时,*/2效果一样。 - 月份和星期可以用英文名称的前三个字母(不区分大小写),但名称只能单独用,不能出现在范围或列表里。
几个来自手册的示例:
5 0 * * * $HOME/bin/daily.job >> $HOME/tmp/out 2>&1
每天 0:05 执行。
15 14 1 * * $HOME/bin/monthly
每月 1 日 14:15。
0 22 * * 1-5
工作日(星期一到星期五)晚上 10 点。
23 0-23/2 * * *
每天 0:23、2:23、4:23,依此类推。
特殊写法
八个特殊字符串可以代替前五个字段:
@reboot:启动时运行一次。@yearly、@annually:等于0 0 1 1 *。@monthly:等于0 0 1 * *。@weekly:等于0 0 * * 0。@daily、@midnight:等于0 0 * * *。@hourly:等于0 * * * *。
@reboot 有一个时机问题要注意:“启动”指的是 cron 守护进程启动的时刻,可能早于某些系统守护进程或其它设施启动。如果你的脚本依赖的服务还没起来,@reboot 触发时就会失败。这种情况可以在命令里加重试或延迟。
环境变量与 PATH
cron 会自动设置几个变量:SHELL 设为 /usr/bin/sh(手册原文如此;实测环境里是 /bin/sh),LOGNAME 和 HOME 取自 /etc/passwd 中 crontab 所有者的那一行。HOME 和 SHELL 可以在 crontab 里覆盖,LOGNAME 不能。
最容易出事的是 PATH。在前述实测环境的容器里装了一条 * * * * * env > /tmp/cronenv 的任务,cron 实际执行时的环境是:HOME=/root、LOGNAME=root、PATH=/usr/bin:/bin、PWD=/root、SHELL=/bin/sh。
cron 任务的 PATH 只有 /usr/bin:/bin(本实测环境的结果),/usr/local/bin、/sbin、/usr/sbin 都不在里面。所以你自己装在 /usr/local/bin 下的工具、位于 /sbin 的系统命令,在 cron 里直接写名字是找不到的。两个解决办法:写绝对路径,或者在 crontab 开头加一行 PATH 设置。想知道你的环境里 PATH 具体是什么,可以装一条 * * * * * env > /tmp/cronenv 自己看。
还有个隐蔽的坑:crontab 里环境变量的值不做环境变量替换、不做波浪号展开。PATH=$HOME/bin:$PATH 和 PATH=~/bin:/usr/bin 都不会按你期望的方式工作;A=1、B=2 之后写 C=$A $B 也不会替换。要在 PATH 里加目录就把路径写全。
MAILTO 控制输出发给谁:定义且非空时发给指定用户;定义为空(MAILTO="")时不发邮件;不定义时发给 crontab 所有者。想让任务静默,重定向到文件或 /dev/null 更直接。
百分号的坑
命令部分由 /usr/bin/sh 或你在 crontab 里通过 SHELL 指定的 shell 执行。命令里的百分号 %,除非用反斜杠转义,会被换成换行符,并且第一个 % 之后的所有内容会作为标准输入交给命令。
实测最能说明问题。这条任务在容器里根本没生成日志文件:
* * * * * date +%s >> /tmp/cronout
因为 % 被当成换行,命令在 date + 处被截断。转义之后才正常:
* * * * * date +\%s >> /tmp/cronout 2>&1
文件里得到一行 Unix 时间戳。所以 date +%F、tar 配合格式串、任何带 % 的命令,在 crontab 里都必须写成 \%。
行内注释不是注释
和 cron 命令写在同一行的注释,不会被当成 cron 意义上的注释,而是命令的一部分,交给 shell 处理;环境变量设置行同理。要写注释就单独占一行,以 # 开头。
日与星期同时限定是“或”
这是 crontab 语义里最反直觉的一条:当“日”和“星期”两个字段都被限定(都不是 *)时,匹配条件是两者的“或”,不是“并且”。
手册的例子:30 4 1,15 * 5 会在每月 1 日和 15 日的 4:30 执行,另外每个星期五的 4:30 也会执行——而不是“每月 1 日和 15 日中的星期五”。
想要“并且”的效果,手册给出的办法是在命令里加测试,例如每月第二个星期六:
0 4 8-14 * * test $(date +\%u) -eq 6 && echo "2nd Saturday"
注意这里 %u 必须写成 \%u,就是上一节的坑。
系统级 crontab 与 /etc/cron.d
/etc/crontab 和 /etc/cron.d 下的文件是系统级配置,格式与用户 crontab 相同,只是在五个时间字段之后、命令之前多一个用户名字段。修改这些文件不需要运行 crontab 命令安装,但要编辑它们需要 root 权限(例如用 sudo 打开编辑器)。
在实测环境里,/etc/cron.d 下能看到发行版自带的条目(比如 e2scrub_all),/etc/cron.daily 下有 apt-compat、dpkg。/etc/crontab 里 SHELL 是 /bin/sh,并有注释说明可以覆盖 PATH。
排查清单
任务没跑或者结果不对时,按这个顺序查:
crontab -l确认条目真的装进去了;如果手头有原始文件,用crontab -n 文件名检查语法,该命令只检查、不安装。- 命令里的
%是否都写成了\%。 - 命令是否依赖不在 cron 的 PATH 里的可执行文件(本文实测环境是
/usr/bin:/bin)。换成绝对路径试试。 - 脚本里有没有依赖
$HOME展开、波浪号之类的写法。 - 日和星期是否同时限定——你可能踩了“或”的语义。
- 注释是否写在了命令同一行。
@reboot任务失败时,考虑 cron 启动早于依赖的服务。
systemd timer 替代
systemd 的 timer 单元也能做定时执行,相比 crontab 多两样东西:日历表达式能在命令行预览下次触发时间;Persistent= 选项能补跑错过的触发。
先说预览。systemd-analyze calendar 可以离线计算日历表达式的下一次触发时间,不创建任何东西,在没运行 systemd 的容器里也能用:
systemd-analyze calendar 'Mon..Fri *-*-* 09:00'
输出包含 Normalized form:(规范形式)和 Next elapse:(下次触发)两行。几个实测结果:Mon..Fri *-*-* 09:00 的规范形式是 Mon..Fri *-*-* 09:00:00;*-*-* 03:20:00 下次触发是 Mon 2026-10-19 03:20:00 UTC;weekly 规范化为 Mon *-*-* 00:00:00;*:0/15 表示每 15 分钟。表达式语法详见 systemd.time(7)。
timer 需要配一个 service。本站实际部署的一个例子(一个 [Timer] 段):
[Timer]
OnCalendar=*-*-* 03:20:00 Asia/Shanghai
Persistent=false
AccuracySec=1min
Unit=某任务.service
表达式后面可以直接带时区。对应的 service 是 Type=oneshot,也就是单次执行。
Persistent= 默认是 false。设为 true 时,服务上次触发的时间会存在磁盘上;定时器被激活时,如果停止的这段时间里本该至少触发一次,就立即触发服务。这正好处理“关机期间错过的任务补跑”,比如备份、日志清理。它只对配置了 OnCalendar= 的定时器有效。手册还提醒:卸载定时器前应使用 systemctl clean --what=state 定时器单元名.timer 清除它在磁盘上保存的时间戳,其中定时器单元名换成你实际的 timer unit 名称,否则重装同名定时器可能读到旧状态。
另一点要知道:定时器不一定在 OnCalendar= 配置的精确时刻触发,有 AccuracySec= 等精度窗口的因素。
怎么选
以下是我的建议,不是官方结论。简单的、错过一次也无所谓、机器一直开着的任务,crontab 写起来最快,五分钟搞定。符合下面任一情况的,考虑 systemd timer:任务错过必须补跑(用 Persistent=true);时间规则复杂,想先在命令行验证表达式;你已经在用 systemd 管理同类的服务单元,想让定时任务用同样的方式管理。两种方式可以并存,按任务逐个决定即可。
延伸阅读
man 5 crontab、man 1 crontab 和 man systemd.timer,是本文所有结论的出处。



