如果你使用 Linux 已有一段时间,大概已经熟悉 cron 作业(cron jobs)。它可以在每天凌晨 2 点运行脚本,或每隔 5 分钟运行一次。大多数现代 Linux 发行版(Debian、Ubuntu、Fedora、Arch、RHEL)都用 systemd 作为 init 系统,而 systemd 有自己的任务调度方式,称为 定时器单元(timer units)。
Systemd 定时器和 cron 做的是同一件事,但它接入了 systemd 的其余部分:可以拿到日志、依赖处理和资源控制,不用额外拼装。
一个 systemd 定时器总是由两部分组成:
.timer单元,决定任务何时运行。.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. 初学者常犯的错误(以及如何避免)
- 跳过
daemon-reload。 编辑单元文件后,systemd 内存里仍是旧版本。始终重新加载。 - 启用的是服务而不是定时器。 这样作业会立刻跑一次,之后再也不按计划跑。
- 混淆
OnUnitActiveSec=和OnUnitInactiveSec=。 主要咬到长时间运行的服务,第 3.2 节讲过。 - 漏掉
Persistent=true。 在笔记本上,这意味着每日作业在计划时间机器睡着时会悄悄跳过。 - 忘记
[Install]一节。 没有WantedBy=timers.target,systemctl enable就没有可挂的钩子,定时器不会在启动时开始。 - 出问题后不检查状态。 一个 failed 的
oneshot作业会显示为failed,定时器下个周期照样再触发。但除非查日志或设了OnFailure=,否则你注意不到。 - 指望
OnCalendar=在精确的秒上触发。 第 3.1 节讲过,默认的AccuracySec=1min意味着作业可能偏移最多一分钟。这是正常的,不是 bug。 - 因为
daemon-reload没抱怨就信任单元文件。 systemd 可能静默忽略拼错指令而不是报错。编辑后跑systemd-analyze verify来抓住它。 - 假设挂住的作业最终会自己被杀掉。 默认不会。如果脚本可能停滞(过期网络挂载、等一个永远不会来的输入),在
[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. 初学者常犯的错误(以及如何避免)
- 跳过
daemon-reload。 编辑单元文件后,systemd 内存里仍是旧版本。始终重新加载。 - 启用的是服务而不是定时器。 这样作业会立刻跑一次,之后再也不按计划跑。
- 混淆
OnUnitActiveSec=和OnUnitInactiveSec=。 主要咬到长时间运行的服务,第 3.2 节讲过。 - 漏掉
Persistent=true。 在笔记本上,这意味着每日作业在计划时间机器睡着时会悄悄跳过。 - 忘记
[Install]一节。 没有WantedBy=timers.target,systemctl enable就没有可挂的钩子,定时器不会在启动时开始。 - 出问题后不检查状态。 一个 failed 的
oneshot作业会显示为failed,定时器下个周期照样再触发。但除非查日志或设了OnFailure=,否则你注意不到。 - 指望
OnCalendar=在精确的秒上触发。 第 3.1 节讲过,默认的AccuracySec=1min意味着作业可能偏移最多一分钟。这是正常的,不是 bug。 - 因为
daemon-reload没抱怨就信任单元文件。 systemd 可能静默忽略拼错指令而不是报错。编辑后跑systemd-analyze verify来抓住它。 - 假设挂住的作业最终会自己被杀掉。 默认不会。如果脚本可能停滞(过期网络挂载、等一个永远不会来的输入),在
[Service]加TimeoutStartSec=,让 systemd 杀掉它而不是阻塞之后每次运行,见第 7 节。
13. systemd 定时器快速参考
更多关于 systemd Timers 的细节,读手册页。
man systemd.timer看完整指令列表man systemd.time看日历和时间跨度语法man systemd.service看服务选项,比如Type=和沙盒指令
特色图片由 OpenClipart-Vectors 提供,来自 Pixabay。



