如果你使用 Linux 已有一段时间,大概已经熟悉 cron 作业(cron jobs)。它可以在每天凌晨 2 点运行脚本,或每隔 5 分钟运行一次。大多数现代 Linux 发行版(Debian、Ubuntu、Fedora、Arch、RHEL)都用 systemd 作为 init 系统,而 systemd 有自己的任务调度方式,称为 定时器单元(timer units)。

Systemd 定时器和 cron 做的是同一件事,但它接入了 systemd 的其余部分:可以拿到日志、依赖处理和资源控制,不用额外拼装。

一个 systemd 定时器总是由两部分组成:

  1. .timer 单元,决定任务何时运行。
  2. .service 单元,决定任务做什么。

定时器的唯一职责是在正确时间启动服务。理解这个拆分后,其余内容就好懂了。

现在,在写任何东西之前,先运行 systemctl list-timers。你几乎肯定会看到系统已经在跑的定时器,像 apt-daily.timer、logrotate.timer 或 man-db.timer。

$ systemctl list-timers
NEXT                                 LEFT LAST                              PASSED UNIT                           ACTIVATES
Tue 2026-08-25 11:30:00 UTC      3min 40s Tue 2026-08-25 11:20:00 UTC     6min ago sysstat-collect.timer          sysstat-collect.service
Tue 2026-08-25 12:21:48 UTC         55min -                                      - fwupd-refresh.timer            fwupd-refresh.service
Tue 2026-08-25 19:37:38 UTC            8h Tue 2026-08-25 11:06:29 UTC            - motd-news.timer                motd-news.service
Tue 2026-08-25 21:35:55 UTC           10h -                                      - apt-daily.timer                apt-daily.service
Wed 2026-08-26 00:00:00 UTC           12h -                                      - dpkg-db-backup.timer           dpkg-db-backup.service
Wed 2026-08-26 00:00:00 UTC           12h -                                      - sysstat-rotate.timer           sysstat-rotate.service
Wed 2026-08-26 00:07:00 UTC           12h -                                      - sysstat-summary.timer          sysstat-summary.service
Wed 2026-08-26 00:45:21 UTC           13h -                                      - logrotate.timer                logrotate.service
Wed 2026-08-26 06:29:14 UTC           19h -                                      - apt-daily-upgrade.timer        apt-daily-upgrade.service
Wed 2026-08-26 11:06:17 UTC           23h -                                      - man-db.timer                   man-db.service
Wed 2026-08-26 11:12:00 UTC           23h Tue 2026-08-25 11:12:00 UTC    14min ago update-notifier-download.timer update-notifier-download.service
Wed 2026-08-26 11:22:00 UTC           23h Tue 2026-08-25 11:22:00 UTC 4min 19s ago systemd-tmpfiles-clean.timer   systemd-tmpfiles-clean.service
Sun 2026-08-30 03:10:01 UTC        4 days -                                      - xfs_scrub_all.timer            xfs_scrub_all.service
Sun 2026-08-30 03:10:34 UTC        4 days -                                      - e2scrub_all.timer              e2scrub_all.service
Mon 2026-08-31 00:55:31 UTC        5 days -                                      - fstrim.timer                   fstrim.service
Sun 2026-09-06 01:25:59 UTC 1 week 4 days -                                      - update-notifier-motd.timer     update-notifier-motd.service
16 timers listed.
Pass --all to see loaded but inactive timers, too.

这些并不特殊。它们是普通 timer units,构建方式和你要做的一样。可以用 systemctl cat man-db.timer 检查其中任意一个,看看生产环境里的真实定时器怎么写。

另外请记住:定时器依赖 systemd 作为 PID 1 运行。

$ ps -p 1
PID TTY          TIME CMD
1 ?        00:00:06 systemd

多数 Docker 容器、某些最小化 chroot 环境以及部分 WSL 配置里 systemd 根本没有运行,因此定时器(以及 systemctl 本身)不额外设置就无法工作。如果你构建的定时器看起来悄悄什么都没做,先检查 ps -p 1,再去调试单元文件。

8. 无需 root 运行定时器

前面都假设是 /etc/systemd/system/ 下的系统级单元。你也可以用 --user 标志以普通用户身份设置定时器,不需要 root。

mkdir -p ~/.config/systemd/user
# put backup.service and backup.timer in that folder
systemctl --user daemon-reload
systemctl --user enable --now backup.timer

注意用户定时器通常只在你的会话活跃时运行。如果希望注销后继续跑,需要启用 lingering(即使是你自己的账户也要 root):

sudo loginctl enable-linger $USER

9. systemd 定时器 vs cron:何时用哪个

定时器的理由:

通过 journalctl 拿到真正的日志,不再需要 MAILTO= 这类 hack 或手动重定向输出。还能声明依赖,比如 After=network-online.target 或 Requires=postgresql.service,cron 根本做不到。

可以用 ProtectSystem=strict 或 PrivateTmp=true 之类选项把服务关进沙盒。还有内置测试(systemd-analyze calendar)和清晰的概览(list-timers),补跑(catch-up)和抖动是原生的,不用脚本外挂。

cron 的理由:

几乎处处能跑,包括没有 systemd 的系统,比如 OpenRC 的 Alpine、BSD 或精简容器。一行(one-line)crontab 条目比写两个单元文件简单得多,尤其对没有任何依赖的任务。

如果团队已有多年 cron 工具链,全量切换可能不划算。

那么该选哪个?

在 systemd 系统上,定时器通常是更好的默认,尤其当你重视日志、错过恢复或依赖顺序时。

对快速、可移植、无依赖的脚本,cron 照样够用。与其说这是规则,不如说是根据环境做的判断。

9.1 把 crontab 行转成 OnCalendar

迁移现有 cron 作业时,这个映射会反复用到。cron 字段顺序是 minute hour day month weekday;OnCalendar= 重排为 weekday year-month-day hour:minute:second。

注意最后一行:@reboot 没有日历等价物,因为它根本不绑定墙上时钟。那正是 OnBootSec= 存在的理由,也是 systemd 定时器能做而 cron 结构上做不到的好例子。

10. 作业失败时收到提醒

几乎每个人在第一个定时器跑通后都会问:"怎么知道它什么时候不灵了?"

用 cron 时,诚实的答案通常是"不知道,直到有人发现 backup 没了。"

systemd 有真正的答案:OnFailure=。

把它加到服务里,指向第二个负责发通知的小服务:

# /etc/systemd/system/backup.service
[Unit]
Description=Back up home directory
OnFailure=alert-on-failure@%n.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/[email protected]
[Unit]
Description=Send a failure alert for %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh "%i"

%n 展开为 failed 的那个单元名,提醒服务里的 %i 接收它。notify-failure.sh 可以简单到对 webhook 发一个 curl 调用,或者调 mail。重点不是通知机制本身,而是 OnFailure= 只在失败时自动触发,零轮询,也不需要额外定时器去检查第一个。

11. 完整示例:每周日志清理

# /etc/systemd/system/logcleanup.service
[Unit]
Description=Purge logs older than 30 days
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/find /var/log/myapp -name "*.log" -mtime +30 -delete
# /etc/systemd/system/logcleanup.timer
[Unit]
Description=Weekly log cleanup
[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target

然后启用它并确认真的被调度了:

sudo systemctl daemon-reload
sudo systemctl enable --now logcleanup.timer
systemd-analyze calendar "Sun *-*-* 04:00:00"   # confirm the schedule is right
systemctl list-timers logcleanup.timer          # confirm systemd knows about it
sudo systemctl start logcleanup.service         # test it right away
journalctl -u logcleanup.service -n 20          # check that it worked

12. 初学者常犯的错误(以及如何避免)

  1. 跳过 daemon-reload。 编辑单元文件后,systemd 内存里仍是旧版本。始终重新加载。
  2. 启用的是服务而不是定时器。 这样作业会立刻跑一次,之后再也不按计划跑。
  3. 混淆 OnUnitActiveSec= 和 OnUnitInactiveSec=。 主要咬到长时间运行的服务,第 3.2 节讲过。
  4. 漏掉 Persistent=true。 在笔记本上,这意味着每日作业在计划时间机器睡着时会悄悄跳过。
  5. 忘记 [Install] 一节。 没有 WantedBy=timers.target,systemctl enable 就没有可挂的钩子,定时器不会在启动时开始。
  6. 出问题后不检查状态。 一个 failed 的 oneshot 作业会显示为 failed,定时器下个周期照样再触发。但除非查日志或设了 OnFailure=,否则你注意不到。
  7. 指望 OnCalendar= 在精确的秒上触发。 第 3.1 节讲过,默认的 AccuracySec=1min 意味着作业可能偏移最多一分钟。这是正常的,不是 bug。
  8. 因为 daemon-reload 没抱怨就信任单元文件。 systemd 可能静默忽略拼错指令而不是报错。编辑后跑 systemd-analyze verify 来抓住它。
  9. 假设挂住的作业最终会自己被杀掉。 默认不会。如果脚本可能停滞(过期网络挂载、等一个永远不会来的输入),在 [Service] 加 TimeoutStartSec=,让 systemd 杀掉它而不是阻塞之后每次运行,见第 7 节。

13. systemd 定时器快速参考

更多关于 systemd Timers 的细节,读手册页。

  • man systemd.timer 看完整指令列表
  • man systemd.time 看日历和时间跨度语法
  • man systemd.service 看服务选项,比如 Type= 和沙盒指令

特色图片由 OpenClipart-Vectors 提供,来自 Pixabay。

1. 基本设置

假设要每天凌晨 3:30 备份一个目录。需要两个文件。下面的系统级单元放在 /etc/systemd/system/ 下,需要 root 权限。

先创建 service unit:

sudo nano /etc/systemd/system/backup.service

加入这些行:

[Unit]
Description=Back up home directory
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=19
IOSchedulingClass=idle
ProtectSystem=strict
ReadWritePaths=/var/backups

再创建 timer unit:

sudo nano /etc/systemd/system/backup.timer

添加这些行:

[Unit]
Description=Run backup.service daily at 3:30 AM
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target

几点说明:

  • timer 和 service 同名(backup)。systemd 靠这个命名匹配关联它们。只有当你自己设置 Unit= 时,才需要把 timer 指向另一个 service。

  • Type=oneshot 适合 大多数自行运行到完成的任务。如果脚本会派生后台进程或自行守护进程,Type=oneshot 无法正确跟踪它,这时应改用 Type=forking 或 RemainAfterExit=true。你自己写的多数脚本不需要这个。

  • Nice=19 和 IOSchedulingClass=idle 告诉内核这个作业是低优先级,不会和更紧急的任务争抢。ProtectSystem=strict 让整个文件系统对该服务只读,除非用 ReadWritePaths= 显式放行某些路径。这些都不是定时器工作的必要条件,但一旦过了"能不能跑起来"的阶段,值得默认加上。

  • 启用并启动的是定时器,不是服务。只有想测试时才直接启动服务。

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

2. 两种调度方式:日历时间和相对时间

这里最容易让初学者困惑,放慢一点。

2.1 日历定时器(OnCalendar=)

这类定时器在时钟的固定时间点触发,和 cron 类似。语法来自 systemd.time(7):

OnCalendar=*-*-* 03:30:00         # every day at 3:30:00 AM
OnCalendar=Mon,Fri *-*-* 09:00:00 # every Monday and Friday at 9 AM
OnCalendar=*-*-01 00:00:00        # the first day of every month, at midnight
OnCalendar=weekly                 # shortcut for Monday at 00:00
OnCalendar=daily                  # shortcut for every day at midnight
OnCalendar=hourly                 # shortcut for the top of every hour
OnCalendar=*:0/15                 # every 15 minutes

有个小技巧能省很多事。不用等它触发就能测试任何表达式:

$ systemd-analyze calendar "Mon,Fri *-*-* 09:00:00"
Original form: Mon,Fri *-*-* 09:00:00
Normalized form: Mon,Fri *-*-* 09:00:00
Next elapse: Mon 2026-08-17 09:00:00 IST
(in UTC): Mon 2026-08-17 03:30:00 UTC
From now: 1 day 18h left

启用新定时器之前先跑一次。这能避免那个经典错误:写出的 OnCalendar 行看着对,实际触发时间却不对。

还有一个互补检查也值得做:

systemd-analyze verify

它会像 systemd 一样加载单元文件,打印任何语法问题,但不会真的启动任何东西。

sudo systemd-analyze verify /etc/systemd/system/backup.timer

这点很重要,因为 systemd 对单元文件里的拼写错误往往很宽容。拼错指令可能被悄悄忽略而不是大声报错,于是定时器安静地不按预期工作。养成每次编辑后运行 systemd-analyze verify 的习惯,和 daemon-reload 一起。

注意 systemd 定时器默认不是精确的。有个隐藏的 AccuracySec= 设置,默认值是 1 分钟,所以 systemd 可以把作业的开始时间向任一方向挪动最多一分钟。它这样做是有意的,把相近的唤醒合并起来省电(笔记本上尤其重要)。

如果确实需要秒级精确,在 [Timer] 一节设置 AccuracySec=1us。初学者构建的几乎一切东西用默认值就好,这条可以忽略。

2.2 相对定时器(OnBootSec=、OnUnitActiveSec= 等)

这类不关心墙上时钟。它们在某个事件发生后经过一段固定时间再触发。cron 没有对应物,值得学好。

这里有个很多人掉进去的坑。OnUnitActiveSec= 和 OnUnitInactiveSec= 听起来几乎一样,但根本不是一回事。对短暂的 oneshot 作业,差别几乎无所谓,因为作业几乎瞬间就启动并结束了。但对长时间运行的服务,两者行为差别很大。OnUnitActiveSec=1h 从服务启动时开始计时;OnUnitInactiveSec=1h 从服务停止时开始计时。混用的话,你的计划会悄悄偏离。

常见模式:启动 10 分钟后跑一次清理脚本,之后每隔 6 hours 再跑一次。

[Timer]
OnBootSec=10min
OnUnitActiveSec=6h

3. 用 Persistent= 补上错过的运行

假设笔记本电脑在 3:30 正睡着,backup.timer 本应触发。什么都没有发生。作业被跳过,除非加这一行:

[Timer]
Persistent=true

有了 Persistent=true,systemd 会记住定时器上次实际触发的时间(存在 /var/lib/systemd/timers/)。如果机器因为关机或休眠错过了一次计划运行,作业会在下次启动后不久补跑一次。这和旧工具 anacron 很像。所以在笔记本和桌面机上几乎默认就该加 Persistent=true,而一直开机的服务器通常不需要。

4. 用 RandomizedDelaySec= 分散负载

设想一下:你管理五十台服务器,全部在午夜运行 OnCalendar=daily。五十台机器在同一秒打向你的 backup 服务器。这就是惊群效应,可能把服务冲垮。

这样修:

[Timer]
OnCalendar=daily
RandomizedDelaySec=1800

每台机器现在会选一个自己的随机延迟,最多 30 分钟,然后才实际运行。负载自然摊开。如果希望每台机器每次都选同样的延迟(而不是每次运行都重新随机),再加 FixedRandomDelay=true(systemd 247 起可用)。

5. 每天都会用到的 systemd 定时器命令

# List all active timers and when each one fires next
systemctl list-timers
# Include timers that are disabled or haven't triggered yet
systemctl list-timers --all
# Check a specific timer's status
systemctl status backup.timer
# Check how the last run of the service went
systemctl status backup.service
# Read the logs from the service the timer triggered
journalctl -u backup.service
# Trigger the service right now, without waiting for the timer
sudo systemctl start backup.service
# Enable at boot and start immediately
sudo systemctl enable --now backup.timer
# Stop and disable it
sudo systemctl disable --now backup.timer
# Run this after you edit any unit file
sudo systemctl daemon-reload

systemctl list-timers 用一行给出干净的摘要:

NEXT                        LEFT     LAST                         PASSED  UNIT           ACTIVATES
Mon 2026-08-17 03:30:00 IST 12h left Sun 2026-08-16 03:30:00 IST  9h ago  backup.timer   backup.service

你立刻能看到下次运行时间、剩余多久、上次运行时间以及触发哪个服务。用 cron 时通常只能信 crontab -l 然后祈祷。这里确实是实打实的进步。

日志方面一个小提醒:journalctl -u backup.service 假设 journal 真的在保留历史。有些最小化或嵌入式发行版镜像在 /etc/systemd/journald.conf 里设置了 Storage=volatile,意味着日志只在内存里,重启就没了。如果日志看起来消失了,先查这个设置。

6. 作业在定时器再次触发时还在运行会怎样

假设 backup.service 通常 5 分钟就跑完,但今晚不知为何,6 hours 之后它还在跑,而定时器又要到点触发了。会发生什么?

不会出坏事,但可能和你想的不一样:systemd 不会启动第二个重叠实例。激活一个已经激活的单元实际上是空操作。定时器下一次的计划运行悄悄过去,服务继续跑直到结束。

对多数任务,这正是想要的安全网,能防止失控堆积。但这也意味着如果作业挂住,你可能丢掉一整天的运行而不出现任何错误。

两个实际要点:

  • 如果任务确实绝不能重叠也绝不能悄悄挂住,在 [Service] 里配上 TimeoutStartSec=(见下面的陷阱列表),让卡住的运行被杀掉而不是阻塞之后每次运行。
  • 如果怀疑作业卡住了,systemctl status backup.service 会把它显示为 active (running) 并且持续时间远超预期。那就是信号。

7. 不写单元文件的快速一次性定时器

有时只想尽快运行一次,不想创建一对 .timer 和 .service。systemd-run 命令可以即时完成:

# Run a command 10 minutes from now
sudo systemd-run --on-active="10min" /path/to/script.sh
# Run at a specific date and time
sudo systemd-run --on-calendar="2026-08-20 22:00:00" /path/to/script.sh
# Run 5 minutes after every boot, as your own user
systemd-run --user --on-boot="5min" /path/to/script.sh

systemd-run 在后台创建一个临时定时器和服务(可以用 systemctl list-timers 看到),并在重启时自动移除。

这对测试一个调度想法,或只需要一次的任务很好。任何需要跨重启继续存在并一直运行的东西,回到前面写正式单元文件的做法。

8. 无需 root 运行定时器

前面都假设是 /etc/systemd/system/ 下的系统级单元。你也可以用 --user 标志以普通用户身份设置定时器,不需要 root。

mkdir -p ~/.config/systemd/user
# put backup.service and backup.timer in that folder
systemctl --user daemon-reload
systemctl --user enable --now backup.timer

注意用户定时器通常只在你的会话活跃时运行。如果希望注销后继续跑,需要启用 lingering(即使是你自己的账户也要 root):

sudo loginctl enable-linger $USER

9. systemd 定时器 vs cron:何时用哪个

定时器的理由:

通过 journalctl 拿到真正的日志,不再需要 MAILTO= 这类 hack 或手动重定向输出。还能声明依赖,比如 After=network-online.target 或 Requires=postgresql.service,cron 根本做不到。

可以用 ProtectSystem=strict 或 PrivateTmp=true 之类选项把服务关进沙盒。还有内置测试(systemd-analyze calendar)和清晰的概览(list-timers),补跑(catch-up)和抖动是原生的,不用脚本外挂。

cron 的理由:

几乎处处能跑,包括没有 systemd 的系统,比如 OpenRC 的 Alpine、BSD 或精简容器。一行(one-line)crontab 条目比写两个单元文件简单得多,尤其对没有任何依赖的任务。

如果团队已有多年 cron 工具链,全量切换可能不划算。

那么该选哪个?

在 systemd 系统上,定时器通常是更好的默认,尤其当你重视日志、错过恢复或依赖顺序时。

对快速、可移植、无依赖的脚本,cron 照样够用。与其说这是规则,不如说是根据环境做的判断。

9.1 把 crontab 行转成 OnCalendar

迁移现有 cron 作业时,这个映射会反复用到。cron 字段顺序是 minute hour day month weekday;OnCalendar= 重排为 weekday year-month-day hour:minute:second。

注意最后一行:@reboot 没有日历等价物,因为它根本不绑定墙上时钟。那正是 OnBootSec= 存在的理由,也是 systemd 定时器能做而 cron 结构上做不到的好例子。

10. 作业失败时收到提醒

几乎每个人在第一个定时器跑通后都会问:"怎么知道它什么时候不灵了?"

用 cron 时,诚实的答案通常是"不知道,直到有人发现 backup 没了。"

systemd 有真正的答案:OnFailure=。

把它加到服务里,指向第二个负责发通知的小服务:

# /etc/systemd/system/backup.service
[Unit]
Description=Back up home directory
OnFailure=alert-on-failure@%n.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/[email protected]
[Unit]
Description=Send a failure alert for %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh "%i"

%n 展开为 failed 的那个单元名,提醒服务里的 %i 接收它。notify-failure.sh 可以简单到对 webhook 发一个 curl 调用,或者调 mail。重点不是通知机制本身,而是 OnFailure= 只在失败时自动触发,零轮询,也不需要额外定时器去检查第一个。

11. 完整示例:每周日志清理

# /etc/systemd/system/logcleanup.service
[Unit]
Description=Purge logs older than 30 days
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/find /var/log/myapp -name "*.log" -mtime +30 -delete
# /etc/systemd/system/logcleanup.timer
[Unit]
Description=Weekly log cleanup
[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target

然后启用它并确认真的被调度了:

sudo systemctl daemon-reload
sudo systemctl enable --now logcleanup.timer
systemd-analyze calendar "Sun *-*-* 04:00:00"   # confirm the schedule is right
systemctl list-timers logcleanup.timer          # confirm systemd knows about it
sudo systemctl start logcleanup.service         # test it right away
journalctl -u logcleanup.service -n 20          # check that it worked

12. 初学者常犯的错误(以及如何避免)

  1. 跳过 daemon-reload。 编辑单元文件后,systemd 内存里仍是旧版本。始终重新加载。
  2. 启用的是服务而不是定时器。 这样作业会立刻跑一次,之后再也不按计划跑。
  3. 混淆 OnUnitActiveSec= 和 OnUnitInactiveSec=。 主要咬到长时间运行的服务,第 3.2 节讲过。
  4. 漏掉 Persistent=true。 在笔记本上,这意味着每日作业在计划时间机器睡着时会悄悄跳过。
  5. 忘记 [Install] 一节。 没有 WantedBy=timers.target,systemctl enable 就没有可挂的钩子,定时器不会在启动时开始。
  6. 出问题后不检查状态。 一个 failed 的 oneshot 作业会显示为 failed,定时器下个周期照样再触发。但除非查日志或设了 OnFailure=,否则你注意不到。
  7. 指望 OnCalendar= 在精确的秒上触发。 第 3.1 节讲过,默认的 AccuracySec=1min 意味着作业可能偏移最多一分钟。这是正常的,不是 bug。
  8. 因为 daemon-reload 没抱怨就信任单元文件。 systemd 可能静默忽略拼错指令而不是报错。编辑后跑 systemd-analyze verify 来抓住它。
  9. 假设挂住的作业最终会自己被杀掉。 默认不会。如果脚本可能停滞(过期网络挂载、等一个永远不会来的输入),在 [Service] 加 TimeoutStartSec=,让 systemd 杀掉它而不是阻塞之后每次运行,见第 7 节。

13. systemd 定时器快速参考

更多关于 systemd Timers 的细节,读手册页。

  • man systemd.timer 看完整指令列表
  • man systemd.time 看日历和时间跨度语法
  • man systemd.service 看服务选项,比如 Type= 和沙盒指令

特色图片由 OpenClipart-Vectors 提供,来自 Pixabay。