当前位置:首页>Linux>Linux 时间子系统深度解析(一):jiffies 回绕与时间比较宏的奥秘

Linux 时间子系统深度解析(一):jiffies 回绕与时间比较宏的奥秘

  • 2026-10-11 05:44:36
Linux 时间子系统深度解析(一):jiffies 回绕与时间比较宏的奥秘

Linux 时间子系统深度解析(一):jiffies 回绕与 time_after 宏的奥秘

前言

在 Linux 内核开发中,时间管理是最基础也最精妙的底层模块。jiffies 作为系统全局滴答计数器,统计系统上电后时钟中断的累加次数。内核定时器、驱动超时、网络重试、状态延时逻辑,几乎全部依赖 jiffies 实现。

但因为jiffes是unsigned long类型数据,会存在溢出情况,不能直接比较,所以几乎所有内核开发者都在用 time_after()、time_before() 系列宏做超时判断,但背后的环形比较原理却没那么直观:

  • 为什么普通 jiffies > timeout 在溢出后会彻底失效?

  • 为什么 (long)(b - a) < 0 一行代码就能搞定环形时间比较?

  • 它的数学边界、安全区间、翻车临界点到底在哪里?

一、问题的起源:固定位宽计数器必然回绕

1.1 jiffies 基本特性

jiffies 是内核全局无符号计数器:

unsigned long jiffies;

每次时钟中断自增 1。

  • HZ:每秒时钟中断次数,常见 1000(1ms 一拍)

  • 32 位系统最大值: 亿

  • HZ=1000 时,约 49.7 天必然溢出归零

1.2 直接大小比较的致命缺陷

jiffies 被定义为 unsigned long,在 C 语言的语义里,unsigned 类型天然暗示“无符号整数可以直接做大小比较”。看到这个定义,写出 if (jiffies > timeout) 是非常自然的逻辑推导。

unsigned long timeout = jiffies + HZ;if (jiffies > timeout) {    // 溢出回绕后逻辑完全反转}

问题出在哪?

假设 jiffies = 0xFFFFFF00(离溢出仅 256 拍),加 1 秒后:

timeout = 0xFFFFFF00 + 1000 = 0x000003E8(溢出回绕成极小值)

此时 jiffies > timeout 立即为真 —— 不是因为超时了,而是 timeout 本身溢出了。

核心原因:线性数轴比较,无法适配环形计数器。

二、内核终极方案:time_after 宏

2.1 内核原版定义

#define time_after(a,b)         \    (typecheck(unsigned long, a) && \     typecheck(unsigned long, b) && \     ((long)((b) - (a))< 0))

语义严格唯一:time_after(a, b) 表示 时间上 a 晚于 b。

  • typecheck:编译期强制类型校验,杜绝类型不匹配引发的未定义行为

  • 核心机制:无符号减法自动取模(模 )+ 强转 long 后通过符号位判断

2.2 标准合法写法(两种都正确)

写法一:判断是否已超时

if (time_after(jiffies, timeout)) {    // 已超时}

写法二:判断是否未超时(业务更常用)

if (time_before(jiffies, timeout)) {    // 未超时} else {    // 已超时}

该宏唯一硬性约束:真实时间差必须严格小于半圈。

三、核心原理:环形计数器数学模型

为何(long)((b) - (a))< 0)这么写就可以解决jiffies回绕问题,下面做下详细推导。

3.1 全局统一常量与规则

全文统一数学模型:

  • 位宽 N,模值 (一整圈)

  • 半圈阈值 

  • 恒等式:

核心规则:

  1. 无符号减法天然对  取模溢出

  2. 无符号结果强转 long:

    结果 < H → 正数

    结果 ≥ H → 负数

  3. 内核安全前提:真实流逝劣弧时间 

3.2 四大场景推导

核心理解:b - a 的无符号结果,代表从 a 顺时针走到 b 的环形弧长。

真实时间永远取短边劣弧,分为「无溢出」和「跨0溢出」两大类场景。

无溢出场景(两点在同侧,无跨0)

场景一:b > a,时间 a 在 b 之前(未超时)

两点间隔 

 强转为正数

time_after(a,b) = false

场景二:b < a,时间 a 在 b 之后(已超时)

两点间隔 

无符号运算:

 强转为负数

time_after(a,b) = true

跨0溢出场景(两点分居0点两侧)

场景三:b < a,跨0、a 在 b 之前(未超时)

数值上 b < a,但计数器已发生回绕。

真实短边时间:

无符号结果:

 正数

time_after(a,b) = false

场景四:b > a,跨0、a 在 b 之后(已超时)

数值上 b > a,a 落在0附近,发生回绕超时。

真实短边时间:

推导可得:

 负数

time_after(a,b) = true

3.3 结论

a、b两个之间间隔在半圈以内(< H)情况下:

  • a 在 b 之前(未超时):场景一、三 →  → 正数 → false

  • a 在 b 之后(已超时):场景二、四 →  → 负数 → true

一行代码完美实现环形时间判断:

直观感受下,以场景四为例:

从上面画的示意图可以简单的理解为: b - a是a顺时针绕到b的弧长。因为a、b之间的真实时间间隔小于H(这个是假设条件)。所以如果a在b之后,那a顺时针绕到b的弧长比如大于H。而大于H无符号数强制转换成long,必然小于0。这就是time_after实现原理。

3.4 临界边界说明(工程关键)

当真实时间差 (刚好等于半圈):

  • a 在后场景:结果碰巧 true(数值对、逻辑临界)

  • a 在前场景:判定错误,本该 false 却返回 true

因此内核强制约束:所有超时业务时间差必须严格小于半圈 H(≤ H-1)。

四、延伸思考:jiffies = -300 顶级设计巧思

/* * Have the 32-bit jiffies value wrap 5 minutes after boot * so jiffies wrap bugs show up earlier. */#define INITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))

内核启动早期将 jiffies 初始化为临近溢出的临界值,让 32 位 jiffies 在开机约 5 分钟后发生回绕,以便尽早暴露回绕相关的 bug。

业务中直接使用 > / < 线性比较的不规范代码,会在开机临界回绕阶段快速暴露问题,避免设备稳定运行数十天后随机崩机。

实际上我自己就犯过这个错:用 jiffies > timeout 比较时间,调试时一切正常,但系统重启几次后偶尔出现诡异超时——原因就是重启后 jiffies 的初始值变了,触发了回绕边界。

五、结语

内核一行极简代码: ((long)((b) - (a)) < 0) 看似简单,实则融合了数论模运算、计算机组成补码特性、工程容错取舍。 它用「最大只能判断半圈时长」的微小代价,换取了:

  • 零开销、纯算术运算
  • 无分支、极致高效
  • 覆盖所有常规业务延时
  • 完美适配环形计数器回绕
有取舍、有边界、极度简洁、极度可靠,这就是 Linux 内核大道至简的经典设计美学。
往期推荐:
文件系统掉电丢数据之谜:性能与可靠性的权衡设计
Linux内核链表:设计、实现
proc文件系统:调试实战

最新文章

随机文章