在 Linux 中使用 Crontab 调度定时任务

Cron 是一个调度守护进程,会在指定的时间间隔执行任务。这些任务称为 cron 作业(cron job),最常用于自动化系统维护、运行备份以及触发周期性脚本。

例如,你可以创建一个 cron 作业来自动化重复性任务,例如备份数据库、使用最新安全补丁更新系统、检查磁盘空间使用情况或发送电子邮件。

Cron 作业可以按分钟、小时、每月的某天、月份、每周的某天,或这些条件的任意组合来调度运行。

正在规划调度?我们的交互式 crontab 生成器能用通俗英语解释任何 cron 表达式,并展示下次运行的时间。

快速参考

如需可打印的快速参考,请参阅 crontab 速查表。

调度场景Cron 表达式
每分钟* * * * *
每 5 分钟*/5 * * * *
每 15 分钟*/15 * * * *
每小时0 * * * *
每天午夜0 0 * * *
每天上午 9 点0 9 * * *
工作日每天上午 9 点0 9 * * 1-5
每周日午夜0 0 * * 0
每月 1 日0 0 1 * *
每年 1 月 1 日0 0 1 1 *
cron 守护进程启动时@reboot

什么是 Crontab 文件

Crontab(cron table)是一个文本文件,用于指定 cron 作业的调度计划。Crontab 文件有两种类型:系统级 crontab 文件和用户级 crontab 文件。

用户级 crontab 文件以用户名命名,其存放位置因发行版而异。在 Red Hat、Fedora 及其衍生版中,crontab 文件保存在 /var/spool/cron 目录;在 Debian、Ubuntu 及其衍生版中,保存在 /var/spool/cron/crontabs

虽然你可以手动编辑用户级 crontab 文件,但建议使用 crontab 命令来进行管理。

/etc/crontab 文件以及 /etc/cron.d 目录下的脚本是系统级 crontab 文件,只能由系统管理员编辑。

在大多数 Linux 发行版中,你还可以把脚本放入 /etc/cron.hourly/etc/cron.daily/etc/cron.weekly/etc/cron.monthly 这几个目录,它们会按目录名称所暗示的时间间隔运行。这些目录有自己的一些规则,下文会进一步说明。

Crontab 语法与运算符

用户级 crontab 文件中的每一行都包含五个由空格分隔的日期和时间字段,后面紧跟要运行的命令:

* * * * * command(s)

- - - - -

| | | | |

| | | | ----- Day of week (0 - 7) (Sunday=0 or 7)

| | | ------- Month (1 - 12)

| | --------- Day of month (1 - 31)

| ----------- Hour (0 - 23)

------------- Minute (0 - 59)

前五个字段可以包含一个或多个用逗号分隔的值,或用一个连字符分隔的值范围。

  • * —— 星号表示任意值。Hour 字段中的星号表示任务每小时运行。
  • , —— 逗号允许你指定一个值列表。例如在 Hour 字段中 1,3,5 表示任务在凌晨 1 点、3 点和 5 点运行。
  • - —— 连字符指定一个值范围。例如在 Day of week 字段中 1-5 表示每个工作日(周一到周五)运行任务。
  • / —— 斜杠指定步长值。例如在 Hour 字段中 */4 表示任务每四小时运行一次,相当于 0,4,8,12,16,20。你也可以使用范围:1-30/10 等同于 1,11,21

步长只在其所应用的字段内求值。在 Hour 字段中写 */23 并不意味着“每 23 小时”,它表示同一天的第 0 小时和第 23 小时,然后计数在午夜重新开始。

每个字段接受固定范围的值:

字段允许值
分钟0-59
小时0-23
每月的某天1-31
月份1-12 或 JAN-DEC
每周的某天0-7 或 SUN-SAT(0 和 7 都表示周日)

名称不区分大小写,因此 Mon、mon 和 MON 都有效。不同实现对名称和范围的列表支持有所不同。在使用 cronie 的 Fedora、RHEL 及其衍生版上,mon-frijan,apr,jul 是有效的;在 Debian、Ubuntu 及其衍生版上,不允许使用名称的范围和列表,因此请使用数字形式,例如 1-5

每月的某天与每周的某天

每月的某天和每周的某天这两个字段的行为与其他三个不同。当两个字段都被限制(即都不是星号)时,cron 会在任意一个字段匹配时运行作业,而不是在两个字段都匹配时运行。

这一点几乎每个人都踩过坑。看下面这一行:

30 4 1,15 * 5 /path/to/script.sh

它读起来像是“在 1 日和 15 日的凌晨 4:30,但仅限于周五”。Cron 将其理解为“每月 1 日和 15 日的凌晨 4:30,加上每周五的凌晨 4:30”。在普通月份中,这大约会运行六次,而不是零次或一次。

由于这条规则,你无法仅凭时间字段表达“每月第一个周一”。将每月的某天限制在前七天,把每周的某天设为星号,并在命令内部判断星期几:

0 7 1-7 * * test $(date +\%u) -eq 1 && /path/to/script.sh

1-7 的范围将作业限制在每月的第一周,而 date +%u 返回星期几(1 表示周一,7 表示周日)。两者结合使脚本只在第一个周一运行。百分号前的反斜杠是必需的,下一节将解释原因。

转义百分号

在 crontab 中,百分号并不是普通字符。Cron 会读取第一个未转义的 % 之前的命令并执行该部分,把其后的所有内容作为标准输入发送给命令,每个额外的 % 会被转换为换行符。

这会悄然破坏任何使用了 date 格式说明符的命令。下面这个备份作业并不会像看起来那样工作:

0 2 * * * /usr/bin/tar -czf /backup/site-$(date +%Y-%m-%d).tar.gz /var/www

Cron 在第一个 % 处将行拆分,因此 shell 收到的是不完整的命令 tar -czf /backup/site-$(date +,并在 tar 启动前因语法错误退出。Cron 把行的其余部分准备为标准输入,但没有正在运行的命令来接收它。要对每个百分号使用反斜杠转义,以便将其传递给 shell:

0 2 * * * /usr/bin/tar -czf /backup/site-$(date +\%Y-\%m-\%d).tar.gz /var/www

当一条命令需要不止几个百分号时,把它移到一个 shell 脚本中,再从 cron 调用该脚本。Cron 不会解析脚本的内容,因此脚本内部无需转义。

系统级 Crontab 文件

系统级 crontab 文件的语法与用户级 crontab 略有不同。它包含一个额外的必填用户字段,用于指定由哪个用户运行该 cron 作业:

* * * * * <username> command(s)

Cron 目录与 anacron

除了 crontab 文件,大多数发行版还提供了一组目录,它们按固定时间表运行脚本,而无需你自己添加任何 crontab 条目。把一个可执行脚本放入 /etc/cron.hourly/etc/cron.daily/etc/cron.weekly/etc/cron.monthly,它就会按该间隔运行。

有两个细节决定脚本是否真正运行:它需要可执行权限位,并且文件名只能包含字母、数字、下划线和连字符。执行这些目录的助手 run-parts 会跳过任何带点的文件名,因此保存为 backup.sh 的脚本会被忽略,而 backup 会运行。

在 Debian、Ubuntu 及其衍生版上,这些目录由 /etc/crontab 中的 run-parts 行驱动:

25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )

test -x /usr/sbin/anacron 这个守卫值得仔细看。如果安装了 anacron,该 cron 行什么都不做,改由 anacron 接管每日、每周和每月的作业。Fedora、RHEL 及其衍生版通过 /etc/cron.d/0hourly 达到相同效果,它每小时运行 /etc/cron.hourly,其中的 0anacron 脚本把更长的间隔交给 anacron。

Anacron 的存在是因为 cron 假设机器在调度时刻处于运行状态。它会记录每个作业上次完成的时间,并在开机后不久补跑被错过的运行。这解释了常见的一个现象:笔记本电脑或台式机上的每日作业从不运行。如果没有 anacron,run-parts 行在 06:25 触发,而此时关机的机器会直接跳过那一天。

预定义宏

有几个特殊的 cron 调度宏可用于指定常见的时间间隔。你可以用这些快捷方式替代五列日期规格:

  • @yearly(或 @annually)—— 每年 1 月 1 日午夜运行一次。等同于 0 0 1 1 *
  • @monthly —— 每月 1 日午夜运行一次。等同于 0 0 1 * *
  • @weekly —— 每周日午夜运行一次。等同于 0 0 * * 0
  • @daily(或 @midnight)—— 每天午夜运行一次。等同于 0 0 * * *
  • @hourly —— 每小时开始时运行一次。等同于 0 * * * *
  • @reboot —— cron 守护进程启动时运行,通常发生在系统引导期间。重启守护进程可能会再次运行该作业。

crontab 命令

crontab 命令允许你安装、查看或打开 crontab 文件进行编辑:

警告
警告:crontab -r 会在无确认的情况下删除整个 crontab。请先用 crontab -l > crontab.backup 备份,或使用 crontab -r -i 在要求确认后再删除。
  • crontab -e —— 编辑 crontab 文件,如果不存在则创建。
  • crontab -l —— 显示 crontab 文件内容。
  • crontab -r —— 删除你当前的 crontab 文件。
  • crontab -r -i —— 在删除前请求确认。-i 选项仅修改 -r,单独使用时无效。
  • sudo crontab -u <username> -e —— 编辑其他用户的 crontab 文件。

crontab 命令使用 VISUALEDITOR 环境变量指定的编辑器打开文件。

Crontab 环境变量

cron 守护进程会自动设置几个环境变量:

  • 默认 PATH 比你交互式 shell 中的短得多,具体值取决于 cron 的实现。在 Debian、Ubuntu 及其衍生版中为 /usr/bin:/bin;在 Fedora、RHEL 及其衍生版(使用 cronie)中为 /usr/bin:/bin:/usr/sbin:/sbin。如果你运行的命令不在该路径中,请使用二进制文件的绝对路径,或在 crontab 顶部设置自定义 PATH。你不能像在普通脚本中那样隐式追加到 $PATH,因为 cron 不会展开这些赋值中的变量。
  • 默认 shell 是 /bin/sh。要使用其他 shell,请在 crontab 顶部设置 SHELL 变量。
  • Cron 从用户的主目录运行命令。可用 HOME 变量覆盖。
  • 默认情况下,输出会通过邮件发送给 crontab 所有者。设置 MAILTO=email@example.com 重定向通知,或设置 MAILTO="" 完全禁用邮件。

Crontab 访问限制

/etc/cron.deny/etc/cron.allow 文件让你可以控制哪些用户能够访问 crontab 命令。每个文件包含一个用户名列表,每行一个。

如果 /etc/cron.allow 存在,只有其中列出的用户可以使用 crontab 命令。当该文件不存在但 /etc/cron.deny 存在时,除 /etc/cron.deny 中列出的用户外,所有人都可以使用该命令。

如果两个文件都不存在,行为取决于 cron 的实现。标准的 Debian 系统允许所有用户使用 crontab,而其他一些实现则限制为仅 root 用户可访问。

Cron 作业示例

如果你正在创建第一个 cron 作业,请使用这个安全的工作流程:

  • 使用 crontab -e 打开你的 crontab。
  • 添加一个简单的测试作业,例如 */5 * * * * date >> /tmp/cron-test.log
  • 保存文件并用 crontab -l 确认已安装。
  • 等待几分钟并验证 /tmp/cron-test.log 已更新。

一旦上述流程成功,下面的示例涵盖了你最可能需要用到的调度。

  • 每个工作日(周一至周五)15:00 运行命令:0 15 * * 1-5 command
  • 每 5 分钟运行脚本并将标准输出重定向到 /dev/null,只通过邮件发送错误:MAILTO=email@example.com */5 * * * * /path/to/script.sh > /dev/null
  • 每周一 15:00 运行两个命令:0 15 * * Mon command1 && command2
  • 每 2 分钟运行一个 PHP 脚本并将输出追加到日志文件:*/2 * * * * /usr/bin/php /path/to/script.php >> /var/log/script.log
  • 每天 8:00 至 16:00 的每个整点运行脚本:0 08-16 * * * /path/to/script.sh
  • 每个季度第一天 6:00 运行脚本:0 6 1 1,4,7,10 * /path/to/script.sh
  • 每月 1 日和 15 日 21:15 运行脚本:15 21 1,15 * * /path/to/script.sh
  • 设置自定义环境变量并每分钟运行命令:HOME=/opt PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SHELL=/bin/bash MAILTO=email@example.com * * * * * command

故障排查

Cron 作业不运行:先用 crontab -l 检查语法,并先在 shell 中手动测试命令。同时用 systemctl status cron(Debian/Ubuntu)或 systemctl status crond(Red Hat/Fedora)确认 cron 守护进程正在运行。

命令在 shell 中能用但在 cron 中不行:Cron 使用精简的 PATH(/usr/bin:/bin)。请使用二进制文件的绝对路径(例如用 /usr/bin/python3 而不是 python3),或在 crontab 顶部设置完整的 PATH。

脚本运行但没有输出或邮件:Cron 通过邮件发送输出,这需要一个可用的邮件传输代理。要可靠地捕获输出,请将其重定向到文件:command >> /var/log/myjob.log 2>&1。要同时重定向 stdout 和 stderr,请参阅 bash 重定向 stderr/stdout 指南。

没有任何作业启动的迹象:Cron 会记录它启动的每个作业,这能将调度问题与命令失败区分开。在 Debian 和 Ubuntu 上,运行 journalctl -u cron --since todaygrep CRON /var/log/syslog;在 Fedora、RHEL 及其衍生版上,运行 journalctl -u crond --since today。如果你的命令出现在日志中,说明 cron 已尽责,失败在命令内部。

作业在不正确的时间运行:Cron 使用系统时区,你可以用 timedatectl 检查。Cronie 支持在 crontab 顶部使用 CRON_TZ=Region/City 以其他时区调度该表。Debian 的 cron 不支持按 crontab 设置调度时区:设置 TZ 只会改变命令环境,而不会改变作业的运行时间。在 Debian 上,请使用系统时区,或在包装脚本中检查目标时区。

运行脚本时权限被拒绝:用 chmod +x /path/to/script.sh 使脚本可执行,并确保 cron 用户对该文件具有读和执行权限。

常见问题

如何编辑我的 crontab?运行 crontab -e。这会在默认编辑器中打开你的用户 crontab。保存并退出即可安装新调度,更改立即生效。

用户 crontab 和 /etc/crontab 有什么区别?用户 crontab(用 crontab -e 管理)以该用户身份运行作业,包含五个时间字段加命令。/etc/crontab/etc/cron.d/ 中的文件是系统级的,包含一个额外的用户名字段,且只能由 root 编辑。

如何将 cron 作业输出重定向到文件?在你的 cron 命令后追加 >> /path/to/logfile.log 2>&12>&1 部分将 stderr 重定向到 stdout,以便两者都捕获在同一个文件中。

如何在系统启动时运行 cron 作业?使用 @reboot 宏而不是时间表达式:@reboot /path/to/script.sh。该作业在 cron 守护进程启动时运行,通常发生在系统引导期间。由于重启守护进程可能会再次运行它,请确保该命令可以安全地重复执行。

结语

Cron 是 Linux 上自动化周期性任务的标准工具。理解五字段时间语法、运算符和环境变量,就能完全掌控作业的运行时间和方式。要列出和管理现有作业,请参阅 crontab 列表指南。