systemd 定时器 AccuracySec —— 没人量过的迟到
systemd 定时器迟到不是故障,是设计——AccuracySec 默认的 1min 留出一分钟窗口,把多个定时器的唤醒合并成一次 CPU 唤醒。实测附收据:闲机上 10 次计划触发变成 5 次,15 秒日历出现 26–34 秒间隔;同一定时器挨着繁忙的 2 秒邻居时,6 个时刻全部命中,最迟晚 119 ms。
你带着systemd 定时器 AccuracySec这个搜索而来:你的 OnCalendar=*:0/15 定时器总在每分钟第 7 秒触发,而 journalctl -u 却说一切正常。一切确实正常——是 systemd 故意挪走了你的唤醒。这篇文章就是量过的那一版:默认值买到什么、付出什么,以及把你的时间表拿回来的那一行。
要让 systemd 定时器严格按计划触发:在定时器的
[Timer]小节里设置AccuracySec=1s(或更低)。默认是 1 分钟——systemd 把每次唤醒对齐到一张粗网格上,最多可晚 60 秒触发,好把多个定时器合进一次 CPU 唤醒。用systemctl show your.timer -p LastTriggerUSec查真实延迟——它每次触发都会更新。
最后测试 2026-11-14 · systemd 262 (262.1-1-arch) · 全新 Arch VM。下面的每个定时器都真跑过;每张收据都逐字粘贴自那台机器。
*:*:00/15)、同一定时器、两台机器。闲机:十次刻度汇成五次触发——窗口吞掉了每个隔次的刻度。繁忙邻居:每次刻度都准点落地,因为窗口里本来就存在一次唤醒。为什么我的 systemd 定时器会迟到?
论点句:默认配置的 systemd 定时器可能比计划时刻最多晚 60 秒才触发,而 list-timers 会毫无怨言地把它叫作“下一次到期”。不是 bug,也不是时钟漂移——是 AccuracySec,默认 1min,它存在的全部意义就是让调度器可以迟到。
$ sudo systemctl show accA.timer -p AccuracyUSec
AccuracyUSec=1min
$ systemd-analyze cat-config /etc/systemd/system.conf 2>/dev/null | grep -i DefaultTimerAccuracy
#DefaultTimerAccuracySec=1min
验证:逐字粘贴自实测 VM——原版 Arch,无用户配置,所以定时器报告的就是编译期默认值。注意属性拼写:unit 文件里写的是 AccuracySec=,systemd 262 把它暴露成 AccuracyUSec。
机制,一段说清:定时器的到期时刻到来时,systemd 并不立刻唤醒 CPU。它算出一个窗口——从计划时刻起,到 AccuracySec 之后为止——然后在窗口里挑一个能让它与其他定时器共用唤醒、或让 CPU 多睡一会儿的点触发。你的 *:0/15 日历说的是“每分钟四次”;调度器听到的是“四次拼车机会”——而在安静的机器上,它大约只会搭上其中一次。
实测下来,默认值能晚多少?
实验,一段说清:一个计划为 OnCalendar=*:*:00/15——每分钟四次预期触发——出厂精度的定时器,在同一台全新 Arch VM 上跑两遍。一次独占一台闲机,一次挨着一个繁忙的 2 秒定时器。每次触发都记录真实执行时间;收据就是原始时间戳。
先看闲机——大多数内务定时器实际生活的环境:
accA 1794634092.128787380
accA 1794634125.384544834
accCo 1794634125.384546104 ← co-timer mark was :30 of the previous minute
accA 1794634152.127459587
accCo 1794634186.135416303 ← co-timer mark was :30 of the previous minute
accA 1794634186.135424133
accA 1794634212.133508500
验证:10 个刻度过去,服务只跑了 5 次——在写着每 15 秒一次的日历上,间隔 26–34 秒(均值 30.0)。相对最近的刻度,每次触发分别晚 12.1、0.4、12.1、1.1、12.1 秒:半个日程,汇进五次唤醒。
同一定时器,同一日历——一个繁忙邻居(一个 2 秒定时器,AccuracySec=1us):
accA sched=1794633795101333 mono=193094591 exec=1794633795.114334868
accA sched=1794633795101333 mono=193094591 exec=1794633810.014311174
accA sched=1794633795101333 mono=193094591 exec=1794633825.116389742
⋮ (6 fires, 6 marks — every one on its mark)
验证:既然每 2 秒就排好一次唤醒,这些唤醒每一次都落在默认定时器的 60 秒窗口之内——于是调度器在第一个可用的唤醒上触发它:踩着刻度,最迟晚 119 ms。余量没有变小;它是被邻居的日程花掉了。
这一对收据就是整套机制的一屏速览:窗口是一个邀约,不是期限——systemd 可以在窗口内任意时刻触发,并且会在窗口覆盖到的最早一次 CPU 唤醒上触发。繁忙的机器:唤醒立刻就有,你的定时器显得很守时。安静的机器:没有现成的唤醒,调度器就把一次唤醒排在窗口的最远端,而窗口内排队的每次到期都搭这班车。闲置的笔记本和孤零零的服务器——大多数 cron 替代品运行的地方——正是安静的那一类。这就是为什么同一个 unit 会“在我桌面上没问题,到了 NAS 上就迟到”。
AccuracySec 实际改变的是什么?
同一日历,同一 VM,一行改动:AccuracySec=100ms。现在唤醒窗口只有十分之一秒宽,定时器落在日历指到的位置:
accB 1794634230.114374054
accB 1794634245.119370330
accB 1794634260.114446983
验证:三次触发,三个刻度——平均晚 116 ms,最迟晚 119 ms,窗口 100 ms;多出来的约 15 ms 是任何 VM 都有的调度损耗。这就是整颗旋钮:窗口宽度,仅此而已。
而合批本身在共跑定时器身上直接可见。在繁忙机器上以默认精度计划在 :30,它从没等满一个窗口——刻度过后 15 ms 就触发,搭的正是旁边 15 秒定时器本来就要做的那次唤醒。两个定时器,一次唤醒:
accCo sched=1794633810000832 mono=207994091 exec=1794633810.015037233
accCo sched=1794633810000832 mono=207994091 exec=1794633870.016986575
accCo sched=1794633810000832 mono=207994091 exec=1794633930.020077789
(accA exec at the same instants: 1794633795.114, 1794633810.014 …)
验证:co 的 :30 刻度同时也是 15 秒定时器的刻度——:30.000 的唤醒早已排好,co 晚 15 ms 搭了上去。在闲机上,即使没有 :30 邻居,它也玩了同一招:落在定时器 A 的唤醒上,误差不超过 1.2 µs(收据见上)。同一个窗口,更聪明的上车方式。
“systemd 不是迟到。它在拼车——一分钟一次唤醒,四位乘客。AccuracySec 决定这班车要坐多满才发车。”
怎样让定时器按计划触发?
一行 drop-in,除了定时器 unit 本身,别的什么都不用重启:
$ sudo systemctl cat accB.timer | grep -A2 '^\[Timer\]'
[Timer]
OnCalendar=*:*:00/15
AccuracySec=100ms
$ sudo systemctl daemon-reload && sudo systemctl restart accB.timer
$ sudo systemctl show accB.timer -p AccuracyUSec,LastTriggerUSec
AccuracyUSec=100ms
LastTriggerUSec=Sat 2026-11-14 05:33:45 UTC
验证:systemctl show 打印的是生效中的窗口——drop-in 压过编译期默认值,而且 LastTriggerUSec 每次触发都会更新。前后对比的收据就是一条查询,不必翻日志。
能压到多低:定时器 C 在 2 秒日历上跑 AccuracySec=1us,22 秒里触发 11 次,11 次中最差抖动 23 ms——亚毫秒级调度随时可得。但在全网铺开之前,先看看代价那一栏:
精度让你付出什么代价?
闲机用五次唤醒伺候了十个刻度;1 µs 的定时器在同样的窗口里会索要七十五次。在笔记本上,这正是默认值要替你省下的那笔开销——合批不是偷懒,它与把磁盘刷写归拢到一起是同一种功耗纪律。服务器无所谓;你的电池在乎。合理的档位:1s 给人能察觉的 deadline,1min(默认值)给察觉不到的内务活,低于 100ms 的一律不碰,除非你量过为什么——量过之后你会发现,一个带 sleep 循环的长驻服务胜过定时器蜂拥。至于笔记本上的 1us,那是把电池改成暖手宝的用法。
这颗旋钮,一眼看清
| AccuracySec | 日历 | 计划触发 | 实际触发 | 发生了什么 |
|---|---|---|---|---|
1min (default) | *:*:00/15, idle box | 10 | 5 | 间隔 26–34 秒,均值 30.0——触发次数减半 |
1min (default) | *:*:00/15, busy neighbor | 6 | 6 | 每个刻度都中,晚 ≤119 ms |
100ms | *:*:00/15 | 3 | 3 | 每个刻度都中,晚 ≤119 ms |
1us | *:*:00/2 | 11 | 11 | 每个刻度都中,晚 ≤22.7 ms |
台架配方——在任何 VM 上复现这些收据
任何服务会记录自身启动时间的定时器都行:ExecStart=/bin/sh -c 'echo timer-run $(date +%%s.%N) >>/var/lib/stamps.log'(unit 文件里要转义 %%)。让一个 OnCalendar=*:*:00/15 的定时器以出厂精度在闲机上跑两分钟——数触发次数;然后加一个 *:*:00/2 的邻居,看着刻度一个个归位。逐次对比用 systemctl show your.timer -p LastTriggerUSec,它每次触发都会更新。服务里的 TRIGGER_TIMER_REALTIME_USEC/_MONOTONIC_USEC 环境变量携带的是定时器 unit 的激活时间戳,不是每次触发的计划时刻——别拿它们干这件事。
实测环境:一台全新 Arch VM(systemd 262, 262.1-1-arch, kernel 7.2.11-arch1-1)——同一个 *:*:00/15 定时器分别以闲机和挨着繁忙邻居两种方式运行,外加 100 ms 和 1 µs 两档精度;收据是原始的 date +%s.%N 时间戳,未作编辑逐字粘贴。上周那篇“journalctl 磁盘占用 —— vacuum 只会舀水,SystemMaxUse 关阀断流”(2026-11-13)里的 journald 上限检查,就是用一个与闲机场景一模一样的定时器跑的——这台台架就是这么挣到排期的。
人工实测台架 · systemd 262 (262.1-1-arch) · 全新 Arch VM · 最后测试 2026-11-14
一行配置定下你的时间表——AccuracySec=1s。一张收据证明它——LastTriggerUSec。