从“10ms 后叫醒我”理解 ARM64 Linux 时间系统
只跟踪一个真实问题:
一个任务请求: “我现在先睡眠,10ms 后请叫醒我。”
如果你真正理解这件事从请求到唤醒的完整路径,那么:
ARM Generic TimerclocksourceclockeventPPItickhrtimerNO_HZ
这些术语就不再是孤立名词,而是这条路径上各自负责一件具体事情的组件。
1. 现实问题:CPU 怎么知道“10ms 已经过去”?
先设想一个没有 timer 的 CPU。
程序可以执行:
for (;;);
但 CPU 本身不会天然知道:
现在过去了几毫秒;某个任务何时应该超时;某个网络包何时需要重传;某个 `sleep(10ms)` 何时结束。
最朴素的办法是让 CPU 自己不停检查:
现在到 10ms 了吗?现在到 10ms 了吗?现在到 10ms 了吗?
这叫 busy wait(忙等) 。
它的问题显而易见:
CPU 不能去运行其他任务;CPU 不能进入低功耗状态;大量电能浪费在重复检查上;多个任务有不同 deadline 时,管理会很困难。
因此,操作系统需要两种能力:
能力一:准确知道“现在是什么时刻”。能力二:提前预约“未来某个时刻,请硬件通知我”。
这两件事看似相近,实际上是不同问题。
2. 先建立直觉:秒表和闹钟
把 ARM64 的计时机制想成两个物件:
秒表: 一直走; 你随时可以问它“现在是多少”。闹钟: 你设置“在某个时刻响”; 到点后它主动提醒你。
对应到 Linux:
最重要的第一结论:
clocksource 是“读时间”的来源;clockevent 是“制造未来事件”的来源。
不要把它们都理解为“timer”。
3. ARM64 硬件提供了什么?
ARM Generic Timer 可以看成:
一个系统级、持续前进的计数器+每个 CPU 自己可设置的闹钟
3.1 系统计数器:硬件秒表
它以固定频率递增。
例如频率是:
62,500,000 Hz
就表示:
每秒增加 62,500,000 个计数。
可以把它想象成:
01234...
永不停地增长。
ARM64 中几个主要寄存器:
| | |
|---|
CNTFRQ_EL0 | | |
CNTPCT_EL0 | | |
CNTVCT_EL0 | | |
CNTP_CVAL_EL0 | | physical timer compare value |
CNTV_CVAL_EL0 | | virtual timer compare value |
CNTP_CTL_EL0 | | |
此时先不必纠结 physical/virtual 的区别。只要知道:
计数器负责持续走时;比较器负责与目标时刻比较;到点后产生 timer interrupt。
4. 从“10ms 后唤醒”看硬件闹钟如何工作
假设:
当前 counter 值:1,000,000counter 频率:100MHz
那么:
10ms = 0.01 秒100,000,000 × 0.01 = 1,000,000 cycles
内核希望 10ms 后收到通知,就设置:
目标时刻 = 当前值 + 1,000,000 = 2,000,000
然后硬件会持续做这件事:
当前 counter 是否达到 2,000,000?
当达到或超过时:
timer 到期→ 发出中断请求→ CPU 被通知
这不是 CPU 在循环检查。
而是:
CPU 去运行其他任务,甚至进入 idle;硬件自己看时间;到点后硬件主动把 CPU 拉回来。
这就是 timer 的价值。
5. 第一张核心图:秒表、闹钟和中断
图1:ARM Generic Timer 从计数到中断的基本路径示意图这张图想表达什么?
计数器 负责客观地记录时间流逝。目标时刻 由内核根据“下一件要做的事”设置。PPI 是 timer 到期后,硬件通知当前 CPU 的方式。中断 让 CPU 不必忙等。
6. 第二个现实问题:系统里不止一个“10ms 后唤醒”
真实 Linux 系统中,未来事件很多:
任务 A:10ms 后睡眠结束任务 B:12ms 后超时网络栈:15ms 后检查重传调度器:4ms 后需要时间处理驱动:20ms 后检查设备状态
但一个 CPU 通常只有一个主要的本地 Generic Timer comparator。
那怎么办?
错误的想象是:
每个软件 timer 都对应一个硬件 timer。
实际上不是。
Linux 的做法是:
把所有未来事件放进软件队列;找出最早到期的那一个;只把“最早 deadline”写进硬件闹钟。
比如:
调度器事件:4ms任务 A:10ms任务 B:12ms网络事件:15ms驱动事件:20ms
硬件 timer 只设置:
4ms 后触发。
4ms 到了,内核处理调度器事件,再重新检查:
现在最早事件是谁?
可能变成:
任务 A:还剩 6ms任务 B:还剩 8ms网络事件:还剩 11ms
于是重新设置硬件闹钟:
6ms 后触发。
7. hrtimer 的本质:软件版“未来事件调度器”
现在再引入术语:
hrtimer
它不是一块硬件,也不是一个单独 timer 寄存器。
它是 Linux 的软件机制,用于维护大量带精确 deadline 的未来事件。
你可以把它理解为:
每 CPU 一个“预约事件管理员”。职责:1. 保存很多未来事件;2. 找到最早到期的事件;3. 通知硬件闹钟只盯住最早 deadline;4. 硬件闹钟响起后,执行所有已经到期的事件;5. 再预约下一次最早 deadline。
7.1 多个软件 timer 与一个硬件 comparator 的关系
图2:多个软件定时器与硬件比较器关系结构图为什么这个设计很重要?
因为它把:
大量软件 timer
复用到:
每 CPU 一个硬件 deadline comparator。
这是一个非常典型的操作系统设计:
软件负责复杂的多任务调度;硬件负责准确地在“最近需要处理的时刻”唤醒 CPU。
8. 回到真实例子:nanosleep(10ms) 的逻辑路径
假设用户调用:
nanosleep(10ms)
概念上,内核做的是:
1. 计算任务应在什么单调时间到期;2. 为这个任务创建一个超时事件;3. 把任务状态改为不可运行;4. 调度器切走,CPU 可运行其他任务;5. 定时器到期;6. timer interrupt 进入内核;7. 内核找到该到期事件;8. 唤醒睡眠任务;9. 调度器在合适的时机让它再次运行。
注意:
“到期” ≠ “任务瞬间运行”
到期仅意味着:
任务从“因为超时不得运行”变成“具备再次参与调度的资格”。
它真正何时获得 CPU,仍取决于:
调度器优先级CPU 是否忙抢占条件实时任务CPU affinity
这就是为什么:
sleep(10ms)
通常是:
至少睡约 10ms,但不保证恰好 10.000000ms 后在用户态继续执行。
9. 一条完整因果链
图3:定时器唤醒时序图
10. PPI 到底是什么?为什么 timer 用它?
上一节讲过 GIC 中断分类。现在结合 timer 重新理解 PPI。
PPI 的全称是:
Private Peripheral Interrupt每 CPU 私有外设中断
不要把“私有”理解成“只能有一个 CPU 使用”。
正确理解:
每一个 CPU 都有自己本地的一份 timer;每个 CPU 也有自己本地的一份 timer interrupt。
例如:
CPU 0 的最近 deadline:4ms 后CPU 1 的最近 deadline:1ms 后CPU 2 当前没有任何 deadline,进入 idleCPU 3 的最近 deadline:20ms 后
它们必须独立设置、独立到期、独立唤醒。
所以 ARM Generic Timer 使用:
每 CPU local timer→ 每 CPU PPI
而不是一个共享的 SPI。
11. 第一次引入源码:timer 从 DT 初始化
现在原理清楚后,看初始化为什么这样写。
ARM64 的时间初始化:
/* arch/arm64/kernel/time.c */void __init time_init(void){ of_clk_init(NULL); timer_probe(); tick_setup_hrtimer_broadcast(); arch_timer_rate = arch_timer_get_rate(); if (!arch_timer_rate) panic("Unable to initialise architected timer.\n"); lpj_fine = arch_timer_rate / HZ; pv_time_init();}
先用白话翻译:
初始化时钟相关资源;查找并初始化系统 timer;准备 timer broadcast;确认 ARM Generic Timer 频率有效;之后系统才能可靠地处理时间和延迟。
其中:
timer_probe();
会根据 Device Tree 找到:
compatible = "arm,armv8-timer";
再调用:
arch_timer_of_init();
注册关系:
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);
所以完整关系是:
DT 中 timer 节点→ compatible = arm,armv8-timer→ timer_probe()→ arch_timer_of_init()→ Linux 注册 ARM Generic Timer
12. arch_timer_of_init() 具体做什么?
源码路径:
drivers/clocksource/arm_arch_timer.c:1399
它不是简单“打开一个 timer”。
它完成几项不同职责:
static int __init arch_timer_of_init(struct device_node *np){ ... for (...) { irq = of_irq_get(...); if (irq > 0) arch_timer_ppi[i] = irq; } rate = arch_timer_get_cntfrq(); arch_timer_of_configure_rate(rate, np); arch_timer_uses_ppi = arch_timer_select_ppi(); ... ret = arch_timer_register(); return arch_timer_common_init();}
翻译成工程语言:
1. 从 Device Tree 获取 timer 对应的中断;2. 获取硬件 counter 频率;3. 决定当前 Linux 使用哪一类 timer/PPI;4. 注册每 CPU 的 timer 中断处理函数;5. 注册“读当前时间”的 clocksource;6. 注册“设置未来唤醒”的 clockevent。
这说明 ARM Generic Timer 同时被 Linux 拆成两个角色:
一部分用作时间读取;另一部分用作未来 deadline 通知。
13. counter frequency 为什么如此重要?
CNTFRQ_EL0 是计数频率。
比如:
CNTFRQ_EL0 = 100,000,000
那么:
100,000,000 次计数 = 1 秒1,000,000 次计数 = 10ms100,000 次计数 = 1ms
如果频率错了:
内核以为 1,000,000 cycles = 10ms实际硬件却让它等了 20ms
所有基于时间的机制都会错:
sleep 超时错误网络重传错误调度时间统计错误音视频定时错误NTP 校时异常性能分析时间轴错误
所以 Linux 在 ARM64 启动协议中依赖 firmware 正确设置该频率。
Device Tree 中也能写:
clock-frequency
但 Linux binding 明确认为这主要用于处理坏 firmware,不应成为正常依赖。
14. clocksource:现在终于可以准确解释它
在“10ms 后唤醒”的主线里,内核至少要知道:
当前是什么时刻?
这由 clocksource 提供。
ARM Generic Timer 注册:
static struct clocksource clocksource_counter = { .name = "arch_sys_counter", .rating = 400, .read = arch_counter_read, .flags = CLOCK_SOURCE_IS_CONTINUOUS,};
其中:
static u64 arch_counter_read(struct clocksource *cs){ return arch_timer_read_counter();}
你可以把这个结构体理解为给 Linux 的一个承诺:
“如果你想知道当前连续时间,就调用我的 read();我会返回一个持续递增的计数;并告诉你该计数的频率、有效位数和可靠性。”
Linux 再把计数转换成纳秒,最终支持:
ktime_get()ktime_get_ns()CLOCK_MONOTONICCLOCK_REALTIME
14.1 读时间和设置闹钟是两条不同的路径
图4:clocksource 与 clockevent 的分工示意图左边:
我现在几点?
右边:
未来什么时候叫我?
15. clockevent:硬件闹钟的 Linux 接口
Linux 把 ARM timer 的“设置下一次到期”能力封装为:
clockevent
它的核心字段包括:
struct clock_event_device { void (*event_handler)(...); int (*set_next_event)(...); ktime_t next_event; ...};
用人话翻译:
| |
|---|
set_next_event | |
event_handler | |
next_event | |
cpumask | |
features | “是否支持 one-shot、periodic、deep idle 等能力”。 |
ARM timer 初始化时:
clk->features = CLOCK_EVT_FEAT_ONESHOT;clk->name = "arch_sys_timer";clk->cpumask = cpumask_of(smp_processor_id());clk->irq = arch_timer_ppi[arch_timer_uses_ppi];clk->set_next_event = sne;
这对应的真实含义:
这个 timer 支持单次闹钟;它属于当前 CPU;它通过当前 CPU 的 timer PPI 产生中断;Linux 可调用 set_next_event 来预约下一个 deadline。
16. one-shot 到底意味着什么?
先对比两种闹钟。
16.1 周期闹钟
每隔 4ms 自动响一次。
即使系统没有任务、没有 timeout、没有网络事件,它也照样响:
4ms:响8ms:响12ms:响16ms:响...
这叫 periodic tick。
优点:
简单。
缺点:
CPU 空闲也会被反复唤醒;耗电;产生无意义中断;影响性能和延迟抖动。
16.2 单次闹钟
请在 17.3ms 后响一次;响完以后由内核决定下一次什么时候再设。
如果系统未来 1 秒内什么都不需要做:
可以不产生 250 次无意义 tick。
这叫 one-shot。
ARM Generic Timer 作为 Linux clockevent 时,关键代码:
clk->features = CLOCK_EVT_FEAT_ONESHOT;
这意味着它天然适合现代 Linux 的按需唤醒模型。
17. ARM timer 如何设置 one-shot deadline?
源码:
static __always_inline void set_next_event( const int access, unsigned long evt, struct clock_event_device *clk){ unsigned long ctrl; u64 cnt; ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk); ctrl |= ARCH_TIMER_CTRL_ENABLE; ctrl &= ~ARCH_TIMER_CTRL_IT_MASK; if (access == ARCH_TIMER_PHYS_ACCESS) cnt = __arch_counter_get_cntpct(); else cnt = __arch_counter_get_cntvct(); arch_timer_reg_write(access, ARCH_TIMER_REG_CVAL, evt + cnt, clk); arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);}
不要被函数名和寄存器吓住,逻辑只有四步:
第一步:读取闹钟控制状态;第二步:确保闹钟已开启,确保中断未被静音;第三步:读取“现在”的 counter;第四步:写入“现在 + 等待长度”作为目标时刻。
例如:
现在:1,000,000 cycles希望:100,000 cycles 后触发写入:CVAL = 1,100,000
这就是的落地实现:
未来 deadline→ 硬件比较器
18. 到点后,IRQ 会经过哪些层?
当 counter 达到比较值:
ARM Timer 触发 PPI→ GIC→ CPU IRQ→ ARM64 exception vector→ GIC IRQ handler→ timer IRQ handler
最终到达:
timer_handler(...)
核心代码:
ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, evt);if (ctrl & ARCH_TIMER_CTRL_IT_STAT) { ctrl |= ARCH_TIMER_CTRL_IT_MASK; arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, evt); evt->event_handler(evt); return IRQ_HANDLED;}
翻译成直观动作:
1. 确认这次 IRQ 确实是 timer 已到期;2. 暂时把闹钟静音;3. 交给 Linux 时间框架处理;4. 时间框架决定下一次什么时候再响。
18.1 为什么一定要先“静音”?
这是非常实用的硬件中断原理。
此刻:
当前 counter 已经 >= CVAL。
如果不先 mask timer interrupt:
硬件会一直认为“闹钟已到期”;CPU 可能刚处理完又立刻收到同一个 timer IRQ;形成中断风暴。
所以先静音:
timer interrupt mask = 1
然后软件处理到期事件,决定下一 deadline,最后重新设置:
新的 CVAL+ enable+ unmask
这就是一次 one-shot timer 的完整闭环。
19. event_handler 是谁?
这取决于 Linux 当前使用哪种时间策略。
早期或低精度模式下,它可能是:
tick_handle_periodic
含义:
“这是一次固定周期 tick,请更新 jiffies、进程时间统计等。”
在 high-resolution / one-shot 模式下,它通常是:
hrtimer_interrupt
含义:
“硬件闹钟响了,请检查 hrtimer 队列,执行所有已经到期的高精度事件,并重新预约下一次最近 deadline。”
因此同一个硬件 PPI:
可以先服务传统 tick;之后再服务 hrtimer 驱动的 one-shot 时间模式。
20. hrtimer IRQ 到来后实际做什么?
高精度路径入口:
void hrtimer_interrupt(struct clock_event_device *dev)
核心逻辑,先不看细节,理解它做的三件事:
1. 根据 clocksource 读取现在时间;2. 执行所有 deadline <= 当前时间的 hrtimer;3. 找到剩余 timer 中最早的 deadline, 并重新设置硬件闹钟。
源码中的对应部分:
now = hrtimer_update_base(cpu_base);__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_HARD);expires_next = hrtimer_update_next_event(cpu_base);tick_program_event(expires_next, 0);
翻译:
更新“现在”→ 跑到期队列→ 找下一次→ 重设 ARM timer
这就是整个时间系统最核心的运行循环。
21. 重新理解 hrtimer 处理流程
图5:hrtimer 中断处理流程图这张图就是:
硬件闹钟Linux 软件 deadline 队列
之间的闭环。
22. Tick:为什么 hrtimer 已经存在,Linux 还需要 tick?
因为 Linux 不只有“用户请求 sleep”这种任务。
系统还需要周期性或逻辑周期性的工作:
推进 jiffies更新 wall time更新进程 CPU 时间统计处理调度器时间相关逻辑运行一些基于 tick 的兼容机制
传统方式是:
每 1/HZ 秒一次 timer IRQ
例如:
HZ = 250→ 每 4ms 一个 tick。
现代高精度 Linux 中,tick 经常变成:
由一个 per-CPU hrtimer 维护的“软件定期事件”。
即:
不是硬件每 4ms 固定触发;而是 hrtimer 到期后决定下一次 tick deadline,再由同一个 ARM hardware timer 唤醒。
22.1 调度 tick 也是 hrtimer
源码:
static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer){ ... tick_sched_do_timer(ts, now); tick_sched_handle(ts, regs); if (unlikely(ts->tick_stopped)) return HRTIMER_NORESTART; hrtimer_forward(timer, now, TICK_NSEC); return HRTIMER_RESTART;}
白话解释:
这是一个“我每隔一个 tick 周期应该再做一次”的软件 timer。到期后:1. 做一次调度 tick 的工作;2. 如果当前 CPU 不需要停 tick;3. 把自己的 deadline 推到下一个周期;4. 再次加入 hrtimer 队列。
所以:
硬件 timer→ 唤醒 hrtimer 框架hrtimer 框架→ 运行 scheduler tick 这个软件定时器scheduler tick→ 再次安排自己的未来 deadline
23. 什么是 NO_HZ?先不要理解成“完全没有 tick”
假设 CPU 进入 idle,未来 500ms 内没有任何事:
没有任务需要唤醒;没有网络 timeout;没有 scheduler deadline;没有驱动定时任务。
传统 periodic tick 仍会:
每 4ms 唤醒一次 CPU。
500ms 内会产生约:
125 次无意义唤醒。
NO_HZ 的思想是:
既然没有任何事需要做,就不要每隔固定周期把 CPU 叫醒。
此时 Linux 可以停掉本 CPU 的 one-shot timer。
源码:
if (expires == KTIME_MAX) { clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT_STOPPED); dev->next_event = KTIME_MAX; return 0;}
它表示:
当前没有未来 deadline,所以停止本 CPU 的硬件闹钟。
不是系统时间停止了,而是:
没有理由产生 timer interrupt。
若后来有新 deadline,例如:
100ms 后需要唤醒某任务。
Linux 再次启动 one-shot timer 并设置到 100ms 后。
24. 空闲 CPU 的真实行为
图6:空闲 CPU 的 one-shot 定时时序图
25. 一个容易忽略但很重要的问题:CPU 深度休眠时,闹钟还走吗?
如果 CPU 进入深度低功耗状态,可能有两种情况:
情况一:timer 仍然在 always-on 电源域中运行。→ deadline 到期后可以自然唤醒 CPU。情况二:该 CPU 的 local timer 也停止。→ 它自己无法在 deadline 到期时叫醒自己。
Device Tree 的:
always-on
描述 timer 是否始终有电。
Linux 中:
arch_timer_c3stop = !of_property_read_bool(np, "always-on");
若 timer 在 deep idle 可能停止,Linux 需要:
tick broadcast
即由:
另一个仍工作的 timer或其他 CPU
负责在需要时唤醒沉睡 CPU。
这说明 NO_HZ 并不是简单的“停止 timer”:
必须同时确保不会错过 future deadline。
26. 回到启动:为什么 ARM Generic Timer 是早期初始化对象?
普通驱动通常可以晚点 probe。
但没有 timer,内核后续很多能力都不可靠:
延迟timeout调度时间维护hrtimer网络重传workqueue 延时工作profilingtimekeeping用户态 sleep
所以它走早期 Device Tree timer probe:
DT:compatible = "arm,armv8-timer"→ timer_probe()→ arch_timer_of_init()→ 注册 PPI→ 注册 clocksource→ 注册 clockevent
它和 GIC 一样属于“基础设施先行”:
GIC 提供中断传递;timer 提供时间与 deadline;普通设备和普通进程随后才能可靠运行。
27. 这一节真正应该记住的五句话
1. clocksource 是秒表:用于读取当前时间。2. clockevent 是闹钟:用于在未来 deadline 到期时触发事件。3. ARM Generic Timer 同时提供秒表和每CPU闹钟能力。4. hrtimer 是软件的 deadline 管理器: 多个软件定时器共享一个硬件比较器。5. Timer PPI 到期后, 内核处理已到期事件,并只为下一件最早事件重新设置硬件闹钟。
如果这五句清晰,那么后面读 arm_arch_timer.c、hrtimer.c、tick-oneshot.c 时就不会迷失。
28. 读源码时应如何对应原理?
现在按“问题 → 代码”的顺序阅读。
| |
|---|
| arch/arm64/kernel/time.c:time_init() |
| DT 如何匹配 ARM Generic Timer? | drivers/clocksource/timer-probe.c:timer_probe() |
| arch_timer_of_init() |
| set_next_event() |
| timer_handler() |
| hrtimer_interrupt() |
| tick_program_event() |
| 如何在没有 future event 时停止 tick? | tick_program_event(KTIME_MAX, ...) |
29. 建议先做的最小实验
不要立即上 ftrace 或 GDB。先确认“秒表”和“闹钟”都存在。
在 ARM64 QEMU Linux 中:
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
常见输出:
arch_sys_counter
这就是“秒表”。
然后:
cat /sys/devices/system/clockevents/clockevent0/current_device
常见输出:
arch_sys_timer
这就是“闹钟”。
最后:
cat /proc/interrupts | grep -Ei 'arch_timer|timer'sleep 2cat /proc/interrupts | grep -Ei 'arch_timer|timer'
你会看到 timer IRQ 计数变化,这证明链路实际在工作:
硬件 timer→ PPI→ GIC→ CPU IRQ→ Linux 时间处理
30. 自测小问题
- 如果 Linux 只有持续递增的 counter,没有可编程 timer comparator,为什么
sleep(10ms) 仍然会很难高效实现? - 如果 Linux 只有固定周期 timer interrupt,没有 clocksource,为什么无法精确知道当前时间?
- 为什么 Linux 不给每个软件 timeout 都分一个硬件 timer?
- timer 到期后,为什么 Linux 要先处理所有到期事件,再重设下一次 deadline?
- 为什么“10ms 到期”不等于任务恰好在 10ms 时恢复用户态运行?
- CPU 空闲时,为什么没有 future deadline 就应该停止 timer event,而不是继续固定 tick?
- timer PPI 为什么天然适合 per-CPU 的 hrtimer 和 scheduler tick?
熊树先生
持续记录 Linux 内核、嵌入式系统与底层技术学习,分享内核源码分析、驱动开发和系统软件实践。
如果你觉得今天这篇有收获,欢迎 点赞、在看、转发 三连,我们下篇见。