在 Linux 内核开发中,时间管理是最基础也最精妙的底层模块。jiffies 作为系统全局滴答计数器,统计系统上电后时钟中断的累加次数。内核定时器、驱动超时、网络重试、状态延时逻辑,几乎全部依赖 jiffies 实现。
但因为jiffes是unsigned long类型数据,会存在溢出情况,不能直接比较,所以几乎所有内核开发者都在用 time_after()、time_before() 系列宏做超时判断,但背后的环形比较原理却没那么直观:
为什么普通 jiffies > timeout 在溢出后会彻底失效?
为什么 (long)(b - a) < 0 一行代码就能搞定环形时间比较?
它的数学边界、安全区间、翻车临界点到底在哪里?
jiffies 是内核全局无符号计数器:
unsigned long jiffies;每次时钟中断自增 1。
HZ:每秒时钟中断次数,常见 1000(1ms 一拍)
32 位系统最大值: 亿
HZ=1000 时,约 49.7 天必然溢出归零
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 本身溢出了。
核心原因:线性数轴比较,无法适配环形计数器。
#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 后通过符号位判断
写法一:判断是否已超时
if (time_after(jiffies, timeout)) {// 已超时}
写法二:判断是否未超时(业务更常用)
if (time_before(jiffies, timeout)) {// 未超时} else {// 已超时}
该宏唯一硬性约束:真实时间差必须严格小于半圈。
为何(long)((b) - (a))< 0)这么写就可以解决jiffies回绕问题,下面做下详细推导。
全文统一数学模型:
位宽 N,模值 (一整圈)
半圈阈值
恒等式:
核心规则:
无符号减法天然对 取模溢出
无符号结果强转 long:
结果 < H → 正数
结果 ≥ H → 负数
内核安全前提:真实流逝劣弧时间
核心理解:b - a 的无符号结果,代表从 a 顺时针走到 b 的环形弧长。
真实时间永远取短边劣弧,分为「无溢出」和「跨0溢出」两大类场景。

两点间隔
强转为正数
time_after(a,b) = false
场景二:b < a,时间 a 在 b 之后(已超时)

两点间隔
无符号运算:
强转为负数
time_after(a,b) = true
场景三: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
a 在 b 之前(未超时):场景一、三 → → 正数 → false
a 在 b 之后(已超时):场景二、四 → → 负数 → true
一行代码完美实现环形时间判断:

当真实时间差 (刚好等于半圈):
a 在后场景:结果碰巧 true(数值对、逻辑临界)
a 在前场景:判定错误,本该 false 却返回 true
因此内核强制约束:所有超时业务时间差必须严格小于半圈 H(≤ H-1)。
/** 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) 看似简单,实则融合了数论模运算、计算机组成补码特性、工程容错取舍。 它用「最大只能判断半圈时长」的微小代价,换取了: