让大量普通超时按时间槽批量到期
Linux 普通定时器把较近超时放进细粒度槽,远期超时放进粗粒度槽,使用位图寻找待处理槽。v6.12 的这套时间轮不再逐层级联迁移。
工程问题#
网络与 I/O 经常设置兜底超时,正常操作完成后就取消;大多数定时器不会真的到期。为每个远期超时长期保持全局精确排序,未必符合这类工作负载对维护成本与精度的取舍。
实际实现#
calc_wheel_index 依据 expires 与 base->clk 的距离选择层级;calc_index 按该层粒度向后取整到期槽。enqueue_timer 将 timer_list 的内嵌链表节点挂到槽内,设置 pending_map 位并更新最早槽时间。collect_expired_timers 按各层时钟取出到期槽的整条链表,随后执行其中回调。
处理流程#
- 按剩余超时时间选择层级,较远期限采用更粗的时间粒度
- 向后取整到所属层的槽边界,得到槽下标与实际 bucket_expiry
- 把内嵌节点加入槽链表,设置对应 pending 位,并在必要时更新下一次唤醒
- 到期时清除槽位标记,取出该槽链表,逐个处理定时器回调
源码片段#
固定版本 · 原文摘录567static inline unsigned calc_index(unsigned long expires, unsigned lvl,568 unsigned long *bucket_expiry)569{570571 /*572 * The timer wheel has to guarantee that a timer does not fire573 * early. Early expiry can happen due to:574 * - Timer is armed at the edge of a tick575 * - Truncation of the expiry time in the outer wheel levels576 *577 * Round up with level granularity to prevent this.578 */579 expires = (expires >> LVL_SHIFT(lvl)) + 1;580 *bucket_expiry = expires << LVL_SHIFT(lvl);581 return LVL_OFFS(lvl) + (expires & LVL_MASK);582}向后取整到层级边界,避免普通范围内因截断造成提前触发。这个规则不能覆盖 calc_wheel_index 对超出容量期限的截断分支。
584static int calc_wheel_index(unsigned long expires, unsigned long clk,585 unsigned long *bucket_expiry)586{587 unsigned long delta = expires - clk;588 unsigned int idx;589590 if (delta < LVL_START(1)) {591 idx = calc_index(expires, 0, bucket_expiry);592 } else if (delta < LVL_START(2)) {593 idx = calc_index(expires, 1, bucket_expiry);594 } else if (delta < LVL_START(3)) {595 idx = calc_index(expires, 2, bucket_expiry);596 } else if (delta < LVL_START(4)) {597 idx = calc_index(expires, 3, bucket_expiry);598 } else if (delta < LVL_START(5)) {599 idx = calc_index(expires, 4, bucket_expiry);600 } else if (delta < LVL_START(6)) {601 idx = calc_index(expires, 5, bucket_expiry);602 } else if (delta < LVL_START(7)) {603 idx = calc_index(expires, 6, bucket_expiry);604 } else if (LVL_DEPTH > 8 && delta < LVL_START(8)) {605 idx = calc_index(expires, 7, bucket_expiry);时间差决定层级,越远的期限落入越粗的层。此处是选择链的前半段;负 delta 和超过最大容量的处理在后续代码中。
655static void enqueue_timer(struct timer_base *base, struct timer_list *timer,656 unsigned int idx, unsigned long bucket_expiry)657{658659 hlist_add_head(&timer->entry, base->vectors + idx);660 __set_bit(idx, base->pending_map);661 timer_set_idx(timer, idx);662663 trace_timer_start(timer, bucket_expiry);664665 /*666 * Check whether this is the new first expiring timer. The667 * effective expiry time of the timer is required here668 * (bucket_expiry) instead of timer->expires.669 */670 if (time_before(bucket_expiry, base->next_expiry)) {671 /*672 * Set the next expiry time and kick the CPU so it673 * can reevaluate the wheel:674 */675 WRITE_ONCE(base->next_expiry, bucket_expiry);676 base->timers_pending = true;entry 是 timer_list 内嵌节点;pending_map 标记非空槽。最早唤醒使用取整后的 bucket_expiry,而不是直接使用原始 expires。
1855{1856 unsigned long clk = base->clk = base->next_expiry;1857 struct hlist_head *vec;1858 int i, levels = 0;1859 unsigned int idx;18601861 for (i = 0; i < LVL_DEPTH; i++) {1862 idx = (clk & LVL_MASK) + i * LVL_SIZE;18631864 if (__test_and_clear_bit(idx, base->pending_map)) {1865 vec = base->vectors + idx;1866 hlist_move_list(vec, heads++);1867 levels++;1868 }1869 /* Is it time to look at the next level? */1870 if (clk & LVL_CLK_MASK)1871 break;1872 /* Shift clock for the next level granularity */1873 clk >>= LVL_CLK_SHIFT;1874 }1875 return levels;1876}按当前层时钟检查 pending 位,将到期槽的链表整体移出。向更高层移动的是检查时钟,没有把定时器向更低层搬运;回调执行在随后路径中。
四段展示粒度取整、层级选择、侵入式链表与位图更新、到期槽提取。层级选择片段仅截取常规层;过期期限和超出最大容量的分支紧随其后,可在完整源码链接中核对。
收益与取舍#
工程分析带来的收益
- 固定层数下,选槽和挂入链表的结构性工作不随定时器数量增长
- 普通超时可按槽批量处理,不必为每个定时器维持精确全序
- 远期定时器直接在所属层到期,省去经典级联时间轮的逐层迁移
付出的代价
- 远期槽更粗,允许额外延迟;它不能替代 hrtimer 的精确到期排序
- 同一槽的回调可能集中触发。插入成本低不代表到期处理、取消同步或锁竞争也总是 O(1)
- 层数和可表示范围受 HZ 等配置影响;超过时间轮容量的期限会被限制到最大范围,不能声称对任意远期时间都保证不提前
可比较的选择
Linux hrtimer 红黑树按 expires 排序,插入和删除是 O(log n),缓存最左节点使读取最早项为 O(1)。适用于需要更细时间分辨率的定时事件,实际触发延迟仍取决于硬件与调度。
QEMU 有序定时器链表QEMU v9.2.0 的相应 timer list 线性扫描插入,成本 O(n),读取表头直接;数据结构简单,但大量活跃定时器的更新成本不同。两者属于不同运行环境,不能据此断言跨项目性能高低。
版本与适用范围#
固定 Linux v6.12 的 kernel/time/timer.c,与 hrtimer 独立。本文依据当前实现的非级联多层时间轮,不套用早期 timer wheel 的级联流程。这里只分析槽组织与处理路径;NO_HZ、CPU 迁移、deferrable timer、PREEMPT_RT 和同步删除有额外规则。
参考来源#
源码、官方文档与原始论文kernel/time/timer.c · enqueue_timer / internal_add_timer
相关实现与比较#
4 个相关案例hrtimer 通过 timerqueue 的红黑树维护到期顺序,并缓存最早到期节点。
QEMU 的 QEMUTimerList 用有序单链表维护定时器,链表头就是下一到期项。
nginx 事件定时器以到期时间为键放入红黑树,事件循环读取最小键确定等待上限。
QEMU dirty bitmap 为磁盘区间记录是否被写入,使增量备份能够只复制发生变化的范围。