05 · 定时器:hard / soft 分流与 expiry_lock
结论:非 RT 内核所有 hrtimer 回调都在时钟中断的 hardirq 上下文执行,timer wheel 回调在 TIMER_SOFTIRQ 里执行;任何慢回调直接推迟所有任务。RT 内核在
hrtimer_init()里把没有显式标记HRTIMER_MODE_HARD的 hrtimer 一律改成 soft 模式,在 HRTIMER_SOFTIRQ(线程上下文)里到期;只有调度 tick、RT/DL 任务的 sleeper 等保留 hard 模式。为了取消正在运行的回调时不自旋等一个可能被抢占的 softirq 线程,RT 给hrtimer_cpu_base和timer_base各加了一把expiry_lock,hrtimer_cancel()/timer_delete_sync()在 RT 上会睡眠。
第一层
先看结论:RT 只让显式 HARD 的定时器留在 hardirq,其余全部进 softirq 线程;RT 任务自己的睡眠唤醒仍走 hardirq,精度不受影响。
第二层
先看结论:分流发生在
hrtimer_init();取消正在运行的回调改为在expiry_lock上睡眠,避免自旋等一个被抢占的 softirq 线程。
中心规则
kernel/time/hrtimer.c:1616-1623:
/*
* On PREEMPT_RT enabled kernels hrtimers which are not explicitly
* marked for hard interrupt expiry mode are moved into soft
* interrupt context for latency reasons and because the callbacks
* can invoke functions which might sleep on RT, e.g. spin_lock().
*/
if (IS_ENABLED(CONFIG_PREEMPT_RT) && !(mode & HRTIMER_MODE_HARD))
softtimer = true;
soft base 是 clock base 数组的上半部分(:1637-1640)。hrtimer_start_range_ns() 在 RT 上检查的是 HRTIMER_MODE_HARD 与 timer->is_hard 的一致性而非 SOFT(:1332-1340)。6.12.107 只有 hrtimer_init()(:1657-1663),hrtimer_setup() 是后来的 API。
谁还留在 hardirq
- 调度 tick、
sched_deadline等核心显式*_HARD。 - RT/DL 任务的 sleeper(
nanosleep、clock_nanosleep、futex 超时):__hrtimer_init_sleeper()(:2019-2044)在 RT 上对rt_or_dl_task_policy(current)的任务加HRTIMER_MODE_HARD。注释(:2022-2039)解释了为什么普通任务不这样做:不可信的用户态可以让上千线程在同一时刻到期,hardirq 里唤醒「一堆」线程会造成延迟尖峰。 - 因此 RT 应用的周期唤醒不经过 softirq 线程,精度与非 RT 相当甚至更稳。
到期路径
hrtimer_interrupt()(:1849-1879):soft 到期时间到了就 raise_softirq_irqoff(HRTIMER_SOFTIRQ)(:1873-1877),随后只跑 HRTIMER_ACTIVE_HARD(:1879)。hrtimer_run_softirq()(:1824-1841)持 softirq_expiry_lock 跑 HRTIMER_ACTIVE_SOFT;__hrtimer_run_queues() 每跑完一个 soft 回调调用 hrtimer_sync_wait_running()(:1817-1819),给等待取消的任务让路。
expiry_lock:取消时不再自旋
:1440-1455 的注释是 RT 定时器设计的核心:在 RT 上,hrtimer_try_to_cancel() 因回调正在运行而失败时,如果调用者在别的 CPU 自旋等待,会产生无界优先级反转;如果调用者就是抢占了回调的那个任务,自旋永远不会结束。所以 hrtimer_cancel_wait_running()(:1456-1480)对 soft 定时器改为 spin_lock_bh(&base->cpu_base->softirq_expiry_lock) 睡眠等待(享受 PI,把 softirq 线程提上来),hard 定时器仍 cpu_relax()。
kernel/time/timer.c 完全同构:timer_base 增加 expiry_lock 与 timer_waiters(:254-256),del_timer_wait_running()(:1586-1607)睡眠等待(TIMER_IRQSAFE 定时器例外),__timer_delete_sync() 在 RT 上要求可抢占上下文(:1660-1665)——timer_delete_sync() 在 RT 上可能睡眠,不能在 raw 临界区调用。expire_timers()(:1839-1850)非 IRQSAFE 回调开中断执行并在每个回调后 timer_sync_wait_running()。
6.12.107 没有的东西
ktimerd/timersd/raise_ktimers_thread:全树无匹配。TIMER_SOFTIRQ 仍由 ksoftirqd / bh_enable 路径处理(timer.c:2447run_timer_softirq())。kernel/time/没有local_lock用户。- POSIX CPU 定时器经 task_work 到期(
posix-cpu-timers.c:1272-1274)是POSIX_CPU_TIMERS_TASK_WORK的通用行为(kernel/time/Kconfig:56-58,x86/arm64 默认开),不是 RT 专属;RT 分支只是处理「收集到期定时器时 tick 仍可能发生」(:1287-1300)。
ARM64:arch timer
- 时钟源
arch_sys_counter(drivers/clocksource/arm_arch_timer.c:252-256,rating 400,CLOCK_SOURCE_IS_CONTINUOUS)——没有 x86tsc=reliable那类稳定性开关,也不需要。 - 每 CPU 的 timer PPI 用
request_percpu_irq()注册(:1251-1269),percpu-devid 中断不参与强制线程化,所以 hrtimer 的 hard 到期路径在 arm64 RT 上仍是真正的 hardirq;驱动里没有任何CONFIG_PREEMPT_RT分支。 - 事件流(evtstrm,
:97-103, 915-981)让 WFE 等待有周期性唤醒,与 RT 无冲突。 arch/arm64/Kconfig:237select HAVE_POSIX_CPU_TIMERS_TASK_WORK——POSIX CPU 定时器在 arm64 默认走 task_work 到期。- 伪 NMI 开启时 arch timer 中断仍是普通 IRQ(不是 NMI),到期路径不变。
- 网络 TSN 调度器
taprio的 hrtimer 是HRTIMER_MODE_ABS(无 HARD 标记,net/sched/sch_taprio.c:1953),RT 下按本章规则进 softirq——软件门控精度下降,见第 04 章。
性能影响
净影响:普通任务的定时器唤醒多一次线程切换;RT 任务不受影响;
hrtimer_cancel/timer_delete_sync在 RT 上可能睡眠。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
| hrtimer 回调对最坏延迟的贡献 | 全部回调时间 | 仅 HARD 回调(都很短) | 核心收益 |
| RT 任务周期唤醒抖动 | 受其它 hrtimer 回调影响 | 不受普通回调影响(HARD sleeper) | RT 上更稳 |
| 普通任务定时器唤醒延迟 | hardirq 直接唤醒 | 多一次 softirq 线程切换(几 µs – 几十 µs,视负载) | 例如 epoll_wait 超时、usleep |
hrtimer_cancel / timer_delete_sync |
自旋等待 | 可能睡眠 | 调用上下文限制更严 |
| 高频定时器负载 | softirq 就地处理 | ksoftirqd 更忙,可被 RT 任务压住 | 网络重传定时器、tickless 关闭时 |
源码定位
| 主题 | 位置 |
|---|---|
| RT 强制 soft | kernel/time/hrtimer.c:1609-1640 |
| 模式常量 | include/linux/hrtimer.h:30-55 |
| 一致性检查 | kernel/time/hrtimer.c:1332-1340 |
| sleeper HARD 规则 | kernel/time/hrtimer.c:2005-2044 |
| 到期分流 | kernel/time/hrtimer.c:1785-1819, 1849-1879, 1824-1841, 2309 |
| expiry_lock(RT) | kernel/time/hrtimer.c:1392-1491;include/linux/hrtimer_defs.h:111, 120-121 |
hrtimer_cancel |
kernel/time/hrtimer.c:1501-1512 |
| timer wheel RT 字段与等待 | kernel/time/timer.c:214-223, 254-256, 1541-1613, 1658-1674, 1839-1850, 2424-2457 |
| POSIX CPU 定时器 | kernel/time/posix-cpu-timers.c:1272-1300;kernel/time/Kconfig:53-58;arch/arm64/Kconfig:237 |
| arm64 arch timer | drivers/clocksource/arm_arch_timer.c:97-103, 252-256, 1251-1269, 1326 |
调优要点
- RT 应用用
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, …)做周期循环:它走 HARD sleeper,避免累积漂移。 skew_tick=1错开各 CPU 的 tick,减少 hardirq 同时到期的排队;nohz_full隔离 CPU 上 tick 本身就停了。- 驱动作者:需要在 hardirq 到期的 hrtimer 必须显式
HRTIMER_MODE_*_HARD,且回调只能用raw_spinlock_t;否则接受 softirq 到期语义。
小结
- 一条规则(
hrtimer.c:1616-1623)决定分流;一条例外(:2041-2043)保住 RT 任务的唤醒精度。 expiry_lock把「等回调结束」从自旋改成带 PI 的睡眠,hrtimer 与 timer wheel 同构。- 6.12.107 没有独立的
ktimerd线程,TIMER_SOFTIRQ 仍走 ksoftirqd / bh_enable 路径。 - 承上启下:到这里,所有「在中断上下文跑的东西」都被搬进了线程;接下来两章处理由此产生的副作用——分配器与 per-CPU 数据失去了「关抢占」这层隐含保护。