暗色模式

Linux crontab 定时任务实战:从五字段语法到日志排查与常见坑

技术教程
2026-08-05
8
0

说到 Linux 服务器上的自动化,cron 是绕不开的基础设施。备份、日志清理、监控采样、定时同步……只要是需要「到点自动干活」的场景,都能看到它的身影。它没有 systemd timer 那么多花哨特性,但足够简单可靠,是运维工具箱里最常用的定时调度器。

这篇文章在 Ubuntu 24.04.4 LTS 上把所有命令真实执行验证了一遍(cron 3.0pl1-184ubuntu2),从五字段语法讲到实战注册、日志验证、常见坑与排障流程,附终端实测截图,可以直接照着抄。文末还会纠正一个流传很广、但在这版系统上已经过时的说法(关于 cron 的 PATH 环境变量)。

cron 是什么:系统内置的定时任务调度器

cron 是一个守护进程,默认每分钟检查一次各用户的任务表,到了执行时间的任务就交给 shell 去跑。Ubuntu/Debian 上运行的是 Debian 维护的 cron 3.0pl1,这是经典的 Paul Vixie cron 的血统,经过 Debian 团队多年持续修补,稳定性和安全性都有保障——Ubuntu 24.04 自带 3.0pl1-184ubuntu2(2024 年 3 月因 xz-utils 后门事件(CVE-2024-3094)做过一次全量重编译),截至 2026 年 Ubuntu 安全公告中该包不存在受影响漏洞。

提醒一个细节:网上很多教程教你用 cron -V 查版本,这在 RHEL/CentOS 系可以,但 Ubuntu 上会直接报 invalid option -- 'V'(实测如此),正确姿势是查软件包信息:

dpkg -l cron | tail -1

五字段语法:一分钟看懂定时表达式

cron 的精髓是那条五字段表达式:

分 时 日 月 星期  命令
字段含义取值范围
小时内的第几分钟0-59
一天中的第几小时0-23
一个月中的第几天1-31
一年中的第几个月1-12(也支持 jan,feb,…)
星期一周中的第几天0-7,0 和 7 都代表周日(也支持 sun,mon,…)

常用符号只有四个:

  • * 任意值,* * * * * 表示每分钟
  • , 列表,1,15 * * * * 表示每小时的第 1 和第 15 分
  • - 范围,9-18 * * * 表示 9 点到 18 点之间每小时
  • / 步长,*/5 * * * * 表示每 5 分钟

几个经典例子:

*/5 * * * *       每 5 分钟
0 2 * * *         每天凌晨 2 点
0 9 * * 1-5       工作日(周一至周五)早上 9 点
0 0 1 * *         每月 1 号零点
30 4 * * 0        每周日凌晨 4:30
5-55/10 * * * *   每小时的 5 分、15 分……55 分,即每 10 分钟

上面最后一条是 Ubuntu 上 sysstat 软件包的真实任务,后面讲系统级任务时还会见到它。

此外还有一组好记的别名:@reboot(开机时执行一次)、@hourly@daily@weekly@monthly@yearly@reboot 常用于开机自动拉起脚本。

拿不准表达式时,用 crontab.guru 在线校验,输入后直接用人话告诉你执行时间。

crontab 管理命令:增删改查

每个用户都有自己的任务表,统一用 crontab 命令管理:

命令作用
crontab -e编辑当前用户的任务表(首次会提示选择编辑器)
crontab -l列出当前用户的任务
crontab -r删除全部任务(强烈建议 crontab -r -i,删除前会确认)
crontab -u 用户名 -l查看其他用户的任务(需要 root)

两个要点:

  1. 永远不要直接编辑 /var/spool/cron/crontabs/ 下的任务表文件。任务表必须通过 crontab 命令写入,因为它会做语法校验,出错的表达式会被拒绝;直接改文件的话,cron 只会静默跳过坏条目。
  2. 任务表在写入的瞬间生效,不需要重启 cron 服务。

cron 服务在 Ubuntu 上叫 cron:

systemctl is-active cron    # 输出 active 即正常运行

我在 Ubuntu 24.04.4 上实测的环境长这样(这也是本文所有演示的起点):

cron 软件包版本与服务状态

截图中依次是:软件包版本 3.0pl1-184ubuntu2、服务状态 active、以及一台全新服务器上 root 用户的空任务表(no crontab for root)。

实战:写一个系统负载记录脚本

理论说完,来点真的。目标:写一个脚本,每分钟把「当前时间 + 系统 1 分钟负载」追加到一个日志文件,然后验证它确实在按计划执行。

编写脚本

cat > /usr/local/bin/cron-demo.sh << 'EOF'
#!/bin/bash
# cron 演示脚本: 输出当前时间与系统 1 分钟负载
echo "$(date '+%F %T') load:$(awk '{print $1}' /proc/loadavg)"
EOF
chmod +x /usr/local/bin/cron-demo.sh

注意两点:

  • awk 的程序 {print $1} 用了单引号,$1 才不会被 bash 提前展开。如果写成双引号 "{print $1}",脚本里的 $1 会被展开成空,awk 收到的程序变成 {print }——它会把整个 loadavg 行原样打出来(这是我实测时踩过的坑,日志里出现一长串 0.00 0.00 0.00 2/170 65230 就是它的症状);
  • 脚本要 chmod +x,路径用绝对路径。

注册任务:两种写法

写法一,echo 管道直接交给 crontab -:

echo '* * * * * /usr/local/bin/cron-demo.sh >> /var/log/cron-demo.log 2>&1' | crontab -

写法二,先把任务写进文件再整体安装(推荐,便于备份与审计):

crontab /tmp/cron-demo.cron

我注册了两条任务,故意留一条不带输出重定向的——下一节你就能看到它的下场:

* * * * * /usr/local/bin/cron-demo.sh >> /var/log/cron-demo.log 2>&1   # 输出重定向到日志文件
* * * * * /usr/local/bin/cron-demo.sh                                  # 什么都不加,等着丢输出

注册完立刻 crontab -l 就能看到任务:

演示脚本与已注册的定时任务

截图里是演示环境的全部家当:脚本共 3 行(第 1 行 shebang、第 2 行注释、第 3 行才是真正的输出语句),任务表里躺着两条每分钟任务。

验证执行:日志里的真相

* * * * * 在每分钟的第 0 秒触发,不用等太久。一两分钟后再看结果:

执行结果与 cron 服务日志

这张截图信息量很大,拆开讲。

上半部分:带重定向的任务把输出写进了 /var/log/cron-demo.log,每分钟一行时间戳 + 负载(2026-08-05 20:15:01 load:0.0420:16:01 load:0.01),说明任务确实在按计划执行,而且输出完整落盘。

下半部分:journalctl -u cron 能看到 cron 服务的全部日志。每行格式是 时间 主机名 CRON[进程号]: (用户名) CMD (实际执行的命令),注意三个细节:

  1. 每条任务的执行记录都在,精确到秒(都是 :01 触发);
  2. 截图中第 2 行那条 CMD (command -v debian-sa1 > /dev/null && debian-sa1 1 1)sysstat 软件包装的系统级任务(后面「系统级任务」一节会讲它)——系统任务和用户任务的执行记录在同一份日志里;
  3. 不带重定向那条任务,日志里出现了关键提示:(CRON) info (No MTA installed, discarding output)——它的输出被丢弃了,一条都没留下。这就是 cron 最大的坑,下面展开。

常见坑:新手翻车现场

坑 1:输出被静默丢弃(No MTA installed)

cron 的传统行为是:任务产生 stdout/stderr 输出时,通过邮件发给任务所有者(MAILTO)。但绝大多数服务器根本没装邮件服务(MTA),于是输出直接被丢弃——日志里只剩一句 No MTA installed, discarding output,任务「干没干活、干得怎样」完全无从得知。

解决办法:所有任务统一重定向输出:

0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

>> 追加写入,2>&1 把标准错误并入同一个文件。这样任务的一切输出都留痕,配合后面的排障流程,一眼定位问题。

副作用:日志文件会无限增长,记得给它配 logrotate 轮转——日志轮转本身就是这套 cron 体系里的经典应用。

坑 2:PATH 环境变量(多数教程已过时)

传统说法:cron 执行环境极简,PATH 只有 /usr/bin:/bin,所以命令必须写绝对路径。这个说法在 Ubuntu 24.04 上已经过时了,我实测验证过——注册一条任务把 cron 环境里的 PATH 打出来:

echo '* * * * * echo PATH=$PATH' | crontab -

下一条执行后,journal 里看到的是完整 PATH(实测输出原样):

Aug 05 20:18:01 demo CRON[65927]: (root) CMD (echo PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin)

原因:Ubuntu 24.04 的 cron 服务以 -P 参数启动(见 /usr/lib/systemd/system/cron.service 中的 ExecStart=/usr/sbin/cron -f -P),cron 会继承 systemd 环境的完整 PATH。这是 2024 年 2 月 cron 3.0pl1-184ubuntu1 引入的变更,很多老教程没有跟上。

但绝对路径依然是好习惯:cron 不加载 ~/.bashrc~/.profile,你的别名、export 出来的自定义变量它一概不认。稳妥做法是在 crontab 顶部显式声明环境:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com

0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

坑 3:% 号会被 cron 吞掉

crontab 文件里 % 是特殊字符,会被 cron 替换成换行符。所以 date +%Y%m%d 直接写进 crontab 行会当场裂开。两个对策:

  • 转义:date +\%Y\%m\%d;
  • 更干净的做法:把 date 的格式串写进脚本内部——本文的演示脚本就是这么干的,所以 crontab 行里一个 % 都没有。

坑 4:脚本自身有问题

路径写错、忘了 chmod +x、脚本语法错误……任务照样「执行」,但什么都没发生。注册之前先手动跑一遍:

sudo -u 任务所属用户 /绝对/路径/脚本.sh
bash -x /绝对/路径/脚本.sh    # 想看每步执行过程就加 -x

系统级任务:/etc/crontab、/etc/cron.d 与 cron.{hourly,daily,weekly,monthly}

以上是用户级任务;系统级任务有另一套组织方式。

/etc/crontab:系统级 crontab,和用户 crontab 唯一的区别是多一个用户名字段(任务以指定用户身份执行)。Ubuntu 24.04 上它长这样(实测原文):

17 *  * * *  root  cd / && run-parts --report /etc/cron.hourly
25 6  * * *  root  test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6  * * 7  root  test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6  1 * *  root  test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }

注意三件事:

  1. /etc/cron.hourly/etc/cron.daily/etc/cron.weekly/etc/cron.monthly 四个目录由 run-parts 触发——目录里每个可执行、无扩展名的脚本都会被依次执行(logrotate、man-db、apt 清理等系统维护任务都住在这里);
  2. test -x /usr/sbin/anacron || 的意思是:如果装了 anacron(为便携机补跑错过的任务),调度权交给 anacron,否则由 cron 兜底;
  3. /etc/cron.d/:独立配置文件目录,格式和 /etc/crontab 一样(有用户名字段),软件包用它在不动系统 crontab 的前提下装自己的任务,修改后即时生效。

sysstat 装在 /etc/cron.d/sysstat 里的真实任务,正好是「范围 + 步长」组合的好例子:

5-55/10 * * * * root command -v debian-sa1 > /dev/null && debian-sa1 1 1
59 23 * * * root command -v debian-sa1 > /dev/null && debian-sa1 60 2

每 10 分钟(5、15、25……55 分)采样一次系统活动数据,每晚 23:59 再轮转一次统计文件——这就是你在 journalctl -u cron 里看到的那条 debian-sa1

排障流程:任务不执行怎么办

按这个顺序查,五分钟内定位 90% 的问题:

  1. 服务在不在:systemctl is-active cron,不是 active 就先 systemctl start cron 再看 journal 里的报错;
  2. 任务到底有没有被调度:journalctl -u cron --since today --no-pager | grep 'CMD'如果对应时间点根本没有 CMD 行,说明任务压根没被启动——问题在表达式语法或任务表本身(用 crontab.guru 校验,或 crontab -e 重写);
  3. 有 CMD 行但没效果:多半是脚本问题——bash -x 手动执行,检查路径、权限、PATH;
  4. 有 CMD 行,脚本也对:查输出去了哪——grep 'MTA' 找到 No MTA installed,说明输出被丢弃,按坑 1 加重定向;
  5. 临时验证环境:加一条每分钟任务打印环境(echo PATH=$PATH),确认 cron 执行环境与预期一致。

cron vs systemd timer:怎么选

cron 不是唯一选择。systemd 的 timer 单元也能做定时任务,而且日志自动进 journald、支持 Persistent=true 补跑停机期间错过的任务、能写依赖关系、加随机延迟防抖动。简单周期任务用 cron 就够了——配置直观、老系统通用、不依赖 systemd;需要补跑、依赖编排、精细控制时,再考虑 systemd timer。

总结

  • cron 每分钟检查一次任务表,五字段 分 时 日 月 星期 + 命令,*,-/ 四个符号覆盖绝大多数需求;
  • 任务表用 crontab 命令增删改查,别手改 /var/spool/cron/crontabs/;
  • 任务输出必须重定向,否则 No MTA installed 之后啥都留不下;
  • Ubuntu 24.04 的 cron 以 -P 启动、继承完整 PATH,老教程的「PATH 只有 /usr/bin:/bin」已过时,但绝对路径 + 显式声明环境依然是好习惯;
  • crontab 里 % 要转义或写进脚本内部;脚本先手动执行验证再注册;
  • 系统级任务分布在 /etc/crontab/etc/cron.d//etc/cron.{hourly,daily,weekly,monthly},run-parts 负责批量执行;
  • 任务不执行,先 journalctl -u cron | grep CMD 判断是「没被调度」还是「执行了没生效」。

参考资料

发表评论

暂无评论,快来抢沙发吧!