Linux Kernel/timer_list 普通定时器

让大量普通超时按时间槽批量到期

Linux 普通定时器把较近超时放进细粒度槽,远期超时放进粗粒度槽,使用位图寻找待处理槽。v6.12 的这套时间轮不再逐层级联迁移。

v6.12调度与资源分配核验 2026-10-11
相关概念

工程问题#

网络与 I/O 经常设置兜底超时,正常操作完成后就取消;大多数定时器不会真的到期。为每个远期超时长期保持全局精确排序,未必符合这类工作负载对维护成本与精度的取舍。

实际实现#

calc_wheel_index 依据 expires 与 base->clk 的距离选择层级;calc_index 按该层粒度向后取整到期槽。enqueue_timer 将 timer_list 的内嵌链表节点挂到槽内,设置 pending_map 位并更新最早槽时间。collect_expired_timers 按各层时钟取出到期槽的整条链表,随后执行其中回调。

处理流程#

  1. 按剩余超时时间选择层级,较远期限采用更粗的时间粒度
  2. 向后取整到所属层的槽边界,得到槽下标与实际 bucket_expiry
  3. 把内嵌节点加入槽链表,设置对应 pending 位,并在必要时更新下一次唤醒
  4. 到期时清除槽位标记,取出该槽链表,逐个处理定时器回调

源码片段#

固定版本 · 原文摘录
kernel/time/timer.cv6.12 · L567–L582
查看原文件
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_index

向后取整到层级边界,避免普通范围内因截断造成提前触发。这个规则不能覆盖 calc_wheel_index 对超出容量期限的截断分支。

kernel/time/timer.cv6.12 · L584–L605
查看原文件
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);
calc_wheel_index — 常规层级

时间差决定层级,越远的期限落入越粗的层。此处是选择链的前半段;负 delta 和超过最大容量的处理在后续代码中。

kernel/time/timer.cv6.12 · L655–L676
查看原文件
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;
enqueue_timer — 挂入槽并标记

entry 是 timer_list 内嵌节点;pending_map 标记非空槽。最早唤醒使用取整后的 bucket_expiry,而不是直接使用原始 expires。

kernel/time/timer.cv6.12 · L1855–L1876
查看原文件
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}
collect_expired_timers

按当前层时钟检查 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 和同步删除有额外规则。

参考来源#

源码、官方文档与原始论文
01
linux · 时间轮设计说明

kernel/time/timer.c · 时间轮设计说明

源码v6.12
02
linux · calc_index / calc_wheel_index

kernel/time/timer.c · calc_index / calc_wheel_index

源码v6.12
03
linux · enqueue_timer / internal_add_timer

kernel/time/timer.c · enqueue_timer / internal_add_timer

源码v6.12
04
linux · collect_expired_timers

kernel/time/timer.c · collect_expired_timers

源码v6.12

相关实现与比较#

4 个相关案例
nginx·事件循环release-1.26.3

nginx 事件定时器以到期时间为键放入红黑树,事件循环读取最小键确定等待上限。

附源码片段 · 调度与资源分配
QEMU·块设备增量备份v9.2.0

QEMU dirty bitmap 为磁盘区间记录是否被写入,使增量备份能够只复制发生变化的范围。

附源码片段 · 存储与数据放置
11 个匹配案例