周期设为 100 ms,实际每轮却约 130 ms;取消返回后,回调又出现。前者是时间基准,后者是寿命协议。
| 查阅目标 | 阅读位置 |
| 计时与事件硬件 | 第 1~3 节 |
| 取消竞态与周期漂移 | 第 4~5 节 |
| tick 与 timerfd 实验 | 第 6~7 节 |
时钟告诉内核时间走到哪里,定时器记录未来应该发生什么,调度器决定已唤醒任务何时实际运行。这三个阶段的延迟必须分开,不能用 clock_getres 返回的纳秒分辨率保证应用一定按纳秒误差醒来。
1. Clocksource 与 clockevent
clocksource 提供可读取的计数器。内核依据计数差、mask 和换算参数更新时间;计数器频率不一定等于 CPU 当前执行频率。
clockevent 负责在未来产生通知。set_next_event() 接收设备相应计数单位的 delta,而非所有设备统一接收纳秒;set_next_ktime() 等接口可使用直接时间值。核心负责换算与设备最小/最大范围等约束。
同一硬件设备可能支持两种职责,也可能分别由不同硬件承担。例如可读取的 TSC 不直接等于传统 APIC timer;TSC deadline 等机制又提供基于相应计数的事件能力。CPU 深度空闲时,本地事件设备可能需要 broadcast 机制协助。见 timekeeping 硬件抽象。
2. Jiffies、HZ 与回绕
jiffies 是内核的粗粒度时间计数,HZ 指定相关 tick 尺度。tickless 系统仍维护逻辑时间,不要求每个 CPU 每秒恰好收到 HZ 次定时中断。
getconf CLK_TCK 返回用户态接口的计时单位,不能用它直接读出 CONFIG_HZ。HZ=250 时一个 jiffy 为 4 ms,也不意味着所有 hrtimer 都只能以 4 ms 编程。
在 32 位 unsigned long 下,2^32 个计数以 HZ=100 约 497 天回绕,以 HZ=1000 约 49.7 天。到期比较应使用 time_after/time_before 等宏,并遵守相应距离范围,不能把 jiffies > expires 用作可移植超时判断。很远的未来区间也不能无限依赖半范围差值比较。
毫秒转换用 msecs_to_jiffies() 等 API,避免自行整数除法把短延迟截断成零,或忽略溢出和上限。
3. Timer wheel 与 hrtimer 为何共存
| 机制 | 组织方式与用途 | 时间语义 |
| timer_list / timer wheel | 分级分桶,适合大量超时且很多会提前取消的场景 | jiffies 尺度,较远到期可采用更粗粒度 |
| hrtimer | 按到期值组织高精度时间事件 | ktime/ns 表示,实际精度依事件硬件和配置 |
定时器不是“必须精确到期执行”的预约。IRQ 关闭、CPU 忙、线程优先级、虚拟机暂停等都会使处理滞后;高精度主要改善到期安排能力,不直接提供硬实时上限。
普通 timer callback 通常在 TIMER_SOFTIRQ 语义下执行,不可任意睡眠。hrtimer 的 hard/soft 模式和 PREEMPT_RT 配置影响实际回调上下文,也不能统一说“hrtimer 总在 HRTIMER_SOFTIRQ”或“总在 hardirq”。
若需执行阻塞工作,可由回调安排合适的普通 workqueue 或唤醒任务。schedule_hrtimeout() 是调用者等待的接口,不会把一个已经在原子上下文的 callback 转换成可睡眠函数。见 hrtimer 设计。
4. 初始化、取消与重装的寿命问题
新代码使用 timer_setup() 初始化 timer_list;当前内核提供 hrtimer_setup() 初始化 hrtimer,较旧目标可能仍需旧式初始化接口,须对照目标头文件。
| 动作 | 能解决什么 | 尚未解决什么 |
| 删除 pending timer | 撤销排队中的到期事件 | callback 可能已开始 |
| 同步删除 | 等待当前活动结束 | 其他路径仍可能重装 |
| 阻止重装 | 关闭未来触发来源 | 对象仍需活到最后访问者退出 |
因此退出协议必须同时协调排队状态、当前回调和未来访问者。
现代 timer_list 的 timer_shutdown_sync() 用于需要同步停止并阻止重装的退出场景,特别是 timer 与 work 互相重新调度时。旧代码中的 del_timer/del_timer_sync 名字及当前 timer_delete 系列应按目标版本核对,不能把普通 del_timer 注释成“同步删除”。
即使用 shutdown,也必须阻止未来路径访问已释放的 timer 对象;“对存活但关闭的 timer 重装被忽略”不等于“可以对悬空 timer 指针调用 API”。hrtimer_cancel 同样需要和发布/重装者协调。
取消时不要持有回调需要的锁
若线程持锁 L 调用同步取消,callback 已经开始并等待 L,则双方会互等。退出应先让新提交路径停止,在不持相互依赖锁的情况下同步等待,然后释放 timer、work 和其共同上下文。
5. 周期任务用哪个基准
假设周期 100 ms、每次处理耗时 30 ms,对比前几轮开始时间:
| 安排方式 | 第 1 轮 | 第 2 轮 | 第 3 轮 |
| 每轮结束后再等 100 ms | 0 | 130 ms | 260 ms |
| 按 t0 + n × 100 ms 安排 | 0 | 100 ms | 200 ms |
这里忽略其他调度延迟。绝对基准避免把工作时间反复加进周期,但实际超时后仍需决定补跑、跳过还是合并。
hrtimer_forward 等接口帮助推进到期点并报告跨越周期,返回 HRTIMER_RESTART 才按相应规则重启;它们不自动为每个漏掉周期执行一次完整业务回调。
用户态也可用 clock_nanosleep(..., TIMER_ABSTIME, ...) 或 timerfd 绝对时间模式减少相对睡眠漂移。选择 MONOTONIC、BOOTTIME 或 REALTIME 又决定校时和 suspend 如何影响截止时间。
固定周期 P=100 ms,把回调工作时间与派发迟延分别加入时间轴。相对重装会把上次工作的结束时间当成新起点,绝对期限则保留原相位。
tnrelative=n(P+C+J)+J,tnabsolute=nP+J
n 从 0 开始,P 是周期,C 是每轮工作时间,J 是本图固定的派发迟延。这里的参数范围没有造成绝对周期过载;真实抖动也不会每次完全相同。
正在呈现知识画面
每次多出来的时间,怎样变成累计漂移?结束后再等一个周期,会把工作和延迟反复计入下次基准;固定绝对相位则不会累积这两项,但仍可能每轮迟到。
{"id":"timer-phase-and-drift","title":"每次多出来的时间,怎样变成累计漂移?","summary":"结束后再等一个周期,会把工作和延迟反复计入下次基准;固定绝对相位则不会累积这两项,但仍可能每轮迟到。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"work\" data-i18n=\"work\"></label><output for=\"work\" id=\"work-value\"></output><input id=\"work\" type=\"range\" min=\"0\" max=\"40\" value=\"30\" step=\"1\"></div><div class=\"control\"><label for=\"delay\" data-i18n=\"delay\"></label><output for=\"delay\" id=\"delay-value\"></output><input id=\"delay\" type=\"range\" min=\"0\" max=\"40\" value=\"0\" step=\"1\"></div><p class=\"caption\" data-i18n=\"relative\"></p><div class=\"plot\"><div class=\"plot-row\"><i id=\"relative-0\"></i><b id=\"relative-label-0\"></b></div><div class=\"plot-row\"><i id=\"relative-1\"></i><b id=\"relative-label-1\"></b></div><div class=\"plot-row\"><i id=\"relative-2\"></i><b id=\"relative-label-2\"></b></div><div class=\"plot-row\"><i id=\"relative-3\"></i><b id=\"relative-label-3\"></b></div><div class=\"plot-row\"><i id=\"relative-4\"></i><b id=\"relative-label-4\"></b></div><div class=\"plot-row\"><i id=\"relative-5\"></i><b id=\"relative-label-5\"></b></div></div><div class=\"axis\"><span>0</span><span>500</span><span>1000 ms</span></div><p class=\"caption\" data-i18n=\"absolute\"></p><div class=\"plot\"><div class=\"plot-row\"><i id=\"absolute-0\"></i><b id=\"absolute-label-0\"></b></div><div class=\"plot-row\"><i id=\"absolute-1\"></i><b id=\"absolute-label-1\"></b></div><div class=\"plot-row\"><i id=\"absolute-2\"></i><b id=\"absolute-label-2\"></b></div><div class=\"plot-row\"><i id=\"absolute-3\"></i><b id=\"absolute-label-3\"></b></div><div class=\"plot-row\"><i id=\"absolute-4\"></i><b id=\"absolute-label-4\"></b></div><div class=\"plot-row\"><i id=\"absolute-5\"></i><b id=\"absolute-label-5\"></b></div></div><div class=\"axis\"><span>0</span><span>500</span><span>1000 ms</span></div><div class=\"metrics\"><div><span data-i18n=\"relError\"></span><strong id=\"relError\"></strong></div><div><span data-i18n=\"absError\"></span><strong id=\"absError\"></strong></div></div><div class=\"formula\" id=\"arithmetic\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.plot-row b{font-size:10px;padding:2px 4px;line-height:1.4;position:absolute;right:3px;top:6px}.plot-row:nth-child(odd){background:var(--accent-soft)}#arithmetic{min-height:5em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const w=+$('#work').value,j=+$('#delay').value;set('work-value',w+' ms');set('delay-value',j+' ms');for(let n=0;n<6;n++){for(const [k,start]of [['relative',n*(100+w+j)+j],['absolute',n*100+j]]){const e=$('#'+k+'-'+n);e.style.left=start/10+'%';e.style.width=w/10+'%';e.style.opacity=w===0?.45:1;set(k+'-label-'+n,n+': '+start+' ms');}}set('relError',5*(w+j)+j);set('absError',j);set('arithmetic','s₅ − 5P = 5 × ('+w+' + '+j+') + '+j+' = '+(5*(w+j)+j)+' ms\\na₅ − 5P = '+j+' ms');set('explanation',t('note'));}$('#work').oninput=$('#delay').oninput=draw;draw();","audio":false,"strings":{"absError":"绝对基准的相位误差 / ms","absolute":"固定相位 nP:同一尺度","delay":"每轮启动延迟 J / ms","heading":"每次多出来的时间,怎样变成累计漂移?","intro":"拖动工作耗时 W 和每轮启动延迟 J。同一时间轴对比六轮开始位置:浅色背景是时间范围,实色短条是实际工作。","note":"两组都允许 J 的启动延迟。区别在于下一轮以原来的相位还是上次完成时刻为基准;并不是绝对定时可以消除调度延迟。","relError":"第 6 轮相位误差 / ms","relative":"结束后再等 P:每行一轮","reset":"重置","scope":"周期 P=100 ms,n=0…5,从 t0=0 开始;每轮固定延迟 J、工作 W,且 W+J<P,无超期补跑争议。相对开始 sₙ=n(P+W+J)+J,绝对开始 aₙ=nP+J。不是实测延迟或 Linux 的上限保证。","scopeLabel":"模型范围与约定","work":"工作耗时 W / ms"}}
6. NO_HZ 减少 tick,不消灭所有中断
NO_HZ_IDLE 在合适的空闲条件下停止周期调度 tick,按下一需要处理的事件唤醒。NO_HZ_FULL 在选定 CPU 和满足条件的负载下进一步减少运行任务时的 tick,但仍有维护工作、RCU、定时器和设备中断。
“只有一个 runnable task”是相关条件之一,不保证任何配置下都永远没有 tick。housekeeping CPU 与任务/IRQ 亲和性也要协同;nohz_full 不是完整 CPU 隔离配置。
cpuidle 的 state*/disable 表示是否允许某些空闲状态,不能直接判断当前 tick 模式。应结合内核配置、启动参数和实际追踪。见 NO_HZ 文档。
7. POSIX timer、itimer 与 timerfd
CLOCK_MONOTONIC/REALTIME 等墙钟类 POSIX timer 与 CPU-time timer 的计时来源不同,不能声称所有 timer_create 都直接对应同一种 hrtimer。SIGEV_SIGNAL、SIGEV_NONE、库实现参与的 SIGEV_THREAD 等通知机制也不同。
setitimer 的 REAL 按经过时间计时,VIRTUAL 按用户 CPU 时间,PROF 按用户与内核 CPU 时间计时;CPU 时间定时器不应当作普通睡眠超时。信号合并、overrun 及处理延迟还会影响应用观察。
timerfd 以 fd 可读性通知,read 得到一个 64 位累计到期次数,不是时间戳,也不保证每次 poll 可读只对应一次到期。下面的程序只创建本进程的 MONOTONIC 定时器,等约 75 ms 后读取 20 ms 周期的累计次数。
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
int main(void)
{
int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
if (fd < 0) { perror("timerfd_create"); return 1; }
struct itimerspec spec = {
.it_interval = { .tv_nsec = 20000000 },
.it_value = { .tv_nsec = 20000000 }
};
if (timerfd_settime(fd, 0, &spec, NULL) < 0) {
perror("timerfd_settime"); close(fd); return 1;
}
struct timespec delay = { .tv_nsec = 75000000 };
while (nanosleep(&delay, &delay) < 0) {
if (errno != EINTR) { perror("nanosleep"); close(fd); return 1; }
}
uint64_t count;
ssize_t n;
do { n = read(fd, &count, sizeof(count)); } while (n < 0 && errno == EINTR);
if (n != sizeof(count)) { fprintf(stderr, "timerfd read failed\n"); close(fd); return 1; }
printf("expirations=%" PRIu64 "\n", count);
close(fd);
return count >= 3 ? 0 : 1;
}
cc -std=c11 -O2 -Wall -Wextra -Werror timer-count.c -o timer-count
./timer-count
典型结果为 3,也可能因调度延迟更多,不能断言始终恰好 3。关闭 fd 回收该定时器;这个例子不修改系统时钟。详见 timerfd。