如果你在内核源码里搜INITIAL_JIFFIES,大概率只能找到一行宏定义,以及某个初始化函数里不起眼的赋值。我第一次看到((unsigned long)(unsigned int)(-300*HZ))的时候愣了好一会儿——Linux内核的时间参数,怎么还允许出现负数?
后来翻启动日志、踩过几次定时器相关的坑,才算把这行宏背后的设计逻辑摸清楚。这确实算个冷门知识点,但它牵扯到jiffies的定义、无符号数回绕、内核启动早期的时钟初始化流程,甚至影响你写驱动时的超时判断方式。这篇文章就把这个“看起来莫名其妙”的宏拆开讲透,适合正在啃内核源码的人,也适合写驱动时被jiffies坑过的嵌入式工程师。
1. 先弄明白 jiffies:内核时间的最小脉搏
1.1 jiffies 到底是什么
jiffies是内核里的一个全局变量,记录系统自启动以来经过的时钟节拍数。内核通过硬件定时器产生周期性中断,每次中断就把jiffies加一,这个中断频率由HZ决定。换句话说,HZ是每秒触发多少次时钟中断,jiffies就是累计的 tick 数。
常见的HZ配置值有 100、250、1000,对应每个 tick 的间隔分别是 10ms、4ms、1ms。多数发行版桌面内核用HZ=1000,服务器内核为了降低中断开销常用HZ=100,嵌入式场景则看实时性需求来选。想确认当前内核的配置,可以这样查:
grep CONFIG_HZ /boot/config-$(uname -r)jiffies本身声明为unsigned long,在 32 位系统上是 32 位,在 64 位系统上是 64 位。内核还有一个 64 位的jiffies_64,通过get_jiffies_64()读取,避免 32 位系统上并发访问导致的高低 32 位不一致问题。
1.2 内核怎么用 jiffies 判断“时间到了”
写驱动时最常见的用法,就是记录一个超时点,然后轮询判断:
#include <linux/jiffies.h> unsigned long timeout = jiffies + 2 * HZ; /* 2 秒后到期 */ while (time_before(jiffies, timeout)) { /* 等待设备就绪,注意加 cpu_relax() 或适当睡眠 */ cpu_relax(); }这里的关键是time_before(),而不是直接写jiffies < timeout。原因是jiffies是无符号数,总会溢出回绕。如果直接比较,一旦jiffies从0xFFFFFFFF回绕到0,所有“未来”的超时点都会瞬间变成“过去”,判断就错误了。
内核提供了一组宏来处理这种回绕:
time_after(a, b):判断a是否在b之后time_before(a, b):判断a是否在b之前time_after_eq()、time_before_eq():带等号版本
它们的实现原理,是把无符号差值强制转换成有符号数再比较。只要两个时间点的间隔不超过 2 的 31 次方个 tick,就能正确判断先后顺序。这个前提在绝大多数场景下都成立,因为 32 位系统上HZ=100时,回绕周期大约是 497 天,而单个等待逻辑很少会超过这个量级。
2. 解剖 INITIAL_JIFFIES:一个“负数”时间起点
2.1 宏定义逐层拆解
在include/linux/jiffies.h里,宏的定义长这样:
#define INITIAL_JIFFIES ((unsigned long)(unsigned int)(-300*HZ))拆开看其实就三步:
-300*HZ是一个负数。HZ=100时是-30000,HZ=1000时是-300000。(unsigned int)(-300*HZ)把这个负数强制转换成无符号 int。这个转换不是“取绝对值”,而是按补码规则重新解释二进制位。比如-30000变成4294937296。- 再转成
unsigned long,在 64 位系统上会做零扩展,得到一个大正数。
以几种常见 HZ 配置为例,INITIAL_JIFFIES的实际数值如下:
| HZ | INITIAL_JIFFIES 数值 | 换算说明 |
|---|---|---|
| 100 | 4294937296 | 相当于 0xFFFF8AD0,靠近 32 位无符号上限 |
| 250 | 4294892296 | 相当于 0xFFFED8F0 |
| 1000 | 4294667296 | 相当于 0xFFFB6C20 |
也就是说,这个"负数"被硬生生解释成了一个非常接近2^32的大整数。
2.2 为什么不能直接把初始值设成 0
不少人第一反应是:既然 jiffies 表示启动以来的 tick 数,那初始值设成 0 不是更自然吗?直接把初值设成 0 会在启动早期带来一个隐蔽问题:如果某段早期代码把jiffies当成“安全时间戳”来记录,当真正的时钟中断还没开始跑的时候,jiffies会一直停留在 0。
举个具体例子。设备驱动在 probe 阶段记录了一个基准点start = jiffies,然后等待设备响应。如果此时jiffies一直是 0,等到时钟中断真正开始递增,jiffies变成 1、2、3…… 那jiffies - start的结果没问题,仍然是正确的消逝时间。问题出在哪呢?
问题出在回绕检测和“是否正常运行”的判断逻辑上。内核里有些代码会通过观察jiffies的数值变化来判断时间子系统是否已经就绪。如果初值为 0,启动阶段jiffies长时间保持不变,某些逻辑可能误判为“时间停滞”或“尚未初始化”。另外,从 0 开始计数意味着系统启动后很快就处于数值最小的区域,一些使用绝对数值做阈值判断的旧代码,容易把“刚启动”误认为“时间已回绕”。
INITIAL_JIFFIES的设计,相当于把 jiffies 的时间零点人为拨到了系统启动之前的 300 秒。这样系统从开机那一刻起,jiffies的数值就从接近2^32的位置开始递减逼近回绕点,而不是从 0 开始递增。第一次回绕被推迟到启动后 300 秒才发生,启动阶段代码无论如何也不会撞上回绕窗口。
2.3 300 秒这个数字是怎么定的
300 秒不是拍脑袋定的。它要满足两个条件:一是必须足够大,保证正常机器的整个启动过程(从电源上电到进入用户态,通常几秒到几十秒)都不会遇到 jiffies 回绕;二是不能太大,避免 jiffies 的绝对数值在启动早期就高到离谱,导致某些基于 jiffies 做时间换算的代码计算出异常结果。
实际内核社区选择300*HZ这个值,是经过长期实践验证的安全余量。早期系统启动需要跑完固件初始化、设备枚举、根文件系统挂载、init 进程启动,正常情况下很少超过 300 秒。即使遇到极端情况,比如某些服务器在 BIOS 阶段就耗了很久,由于时钟中断通常在内核初始化早期就开始运行,jiffies也会同步推进,不太会卡在启动阶段 300 秒不动。
2.4 关键点:这个负偏移不会影响相对时间计算
INITIAL_JIFFIES只是一个固定偏移量,而内核里所有基于 jiffies 的定时、超时运算,本质上都是两个 jiffies 值做差值。偏移量在减法中会自然抵消,所以正常运行阶段完全感觉不到它的存在。
比如:
unsigned long start = jiffies; /* 做一些事情 */ unsigned long elapsed = jiffies - start;无论jiffies初值是 0 还是4294937296,只要start和当前值都基于同一个起点,差值就精确反映了经过的 tick 数。这就是为什么INITIAL_JIFFIES可以放心设成一个看起来奇怪的负偏移,而不会搞乱任何定时功能。
3. 它在启动流程里到底做了什么
3.1 从 start_kernel 到 init_timers 的初始化时机
内核启动早期,start_kernel()会调用一系列初始化函数。jiffies的初值赋值发生在定时器子系统初始化阶段,通常在init_timers()或timekeeping_init()附近。相关代码如下:
void __init init_timers(void) { /* ... 省略其他初始化 ... */ jiffies = INITIAL_JIFFIES; }(不同内核版本的实际位置可能略有差异,你可以直接grep -R "INITIAL_JIFFIES" /usr/src/linux查看。)
在时钟中断真正跑起来之前,所有读取jiffies的代码拿到的都是这个初始值。一旦 tick 中断开始正常工作,jiffies就会每个 tick 加一,从4294937296这种大数开始增长,经过大约 300 秒后增加到0xFFFFFFFF,然后回绕到 0。
3.2 对早期驱动和定时器的实际影响
系统启动阶段,总会有一些驱动在内核初始化过程中注册定时器。比如某个总线驱动要在 50ms 后做一次重试,它会这样写:
mod_timer(&retry_timer, jiffies + msecs_to_jiffies(50));由于jiffies和expires都基于同一个初始偏移量,这行代码计算出的到期时间依然精确。定时器子系统内部用time_after_eq()这类宏判断是否到期,也不会受偏移影响。
真正需要留意的,是那些直接把jiffies当年月日时时间用的场景。有些老驱动、某些固件接口,会假设jiffies在一定范围内,或者直接用jiffies % HZ获取秒内偏移。如果偏移量比较大,这类运算结果会异常。好在内核自身的代码基本都走时间转换接口,这个坑主要在老旧驱动里。
3.3 为什么这段逻辑至今没被删
有人会问:既然现在内核里有ktime、clocksource这么精确的时间接口,jiffies初值还用得着搞这么复杂吗?
答案是用得着。内核维护者不会轻易删除一个影响启动时序的初始值设定,即使它在大多数时候只是一个“背景偏移量”。任何改动都可能导致某些依赖jiffies早期行为的代码出问题,而收益几乎为零。所以这个宏就一直保留着,成了内核源码里“看着没用、实际专门设计过的”那一类存在。
4. 实战:驱动代码里如何正确使用 jiffies
4.1 判断超时的标准姿势
不管你写的驱动是等待 GPIO 信号、寄存器状态还是 DMA 完成,超时判断都建议用内核提供的比较宏。这是我实际项目里的一个典型模式:
#include <linux/delay.h> #include <linux/jiffies.h> static int wait_for_device_ready(struct my_device *dev) { unsigned long timeout = jiffies + msecs_to_jiffies(1000); while (time_before(jiffies, timeout)) { if (readl(dev->regs + STATUS_REG) & DEVICE_READY) return 0; /* 让出 CPU,避免忙等空转 */ usleep_range(1000, 2000); } dev_err(&dev->pdev->dev, "device not ready within 1s\n"); return -ETIMEDOUT; }这里用time_before()而不是直接<,就是为了应对 jiffies 回绕。即使在运行过程中jiffies回绕了一次,只要等待时间小于 2 的 31 次方 tick,判断依然正确。
4.2 计算时间差的正确方法
记录基准时间并计算差值,是驱动开发里最常见的操作。正确做法是直接做无符号减法:
unsigned long start = jiffies; /* 执行某些耗时操作 */ do_something_slow(); unsigned long elapsed_jiffies = jiffies - start; unsigned long elapsed_ms = jiffies_to_msecs(elapsed_jiffies);无符号减法在溢出时也会得到正确结果,因为从start到当前值的 tick 数,在模2^32的意义下就是差值。你可以把jiffies想象成一个里程表:它不关心总里程有多大,只关心两次数值之间转了多少圈。只要差值不超过回绕周期的一半,这个数值就是准确的。
4.3 永远不要给 jiffies 赋“正常值”
我见过有些驱动代码试图在 suspend/resume 流程里“校准” jiffies,直接写:
/* 错误示范 */ jiffies = some_expected_value;这在多数场景下都是灾难。jiffies归内核时间子系统统一管理,驱动直接改值会破坏所有正在等待的定时器、超时判断和延迟计算。如果只是想在系统挂起时修正时间偏差,应该使用timekeeping提供的接口,而不是手写 jiffies。这个教训来自一次实际事故:某驱动在 resume 后把jiffies改写成了 0,结果所有基于jiffies + timeout的定时器全部失效,系统在几分钟内被大量超时中断淹没。
5. 常见误区与排查心得
5.1 误区:jiffies 初始值等于 0
这是最常见的误解。实际上从宏观角度看,系统启动后jiffies的初始值是一个接近 32 位无符号上限的大数,直到开机满 300 秒才会回绕到 0。如果你想验证这一点,可以在一个板子上开机后立即用一个早期 initcall 打印jiffies,会看到它的值通常接近0xFFFFFFFF附近,而不是 0。
5.2 误区:把 jiffies 当作秒级时间戳
jiffies只是 tick 计数器,不是墙上时间。有些人会在应用层通过设备节点读取jiffies,然后自己除以HZ来获取“启动时间”。这种做法有两个问题:一是jiffies会回绕,必须用 64 位版本;二是它只能反映单调递增的时间,不能反映系统休眠期间的真实时间。正确做法是使用ktime_get()系列接口获取单调时钟或实时时钟。
5.3 经典坑:timeout 不更新导致的死循环
我之前排查过一个驱动挂死问题,现象是系统 boot 到一半卡住。日志显示某驱动在等待硬件 FIFO 清空:
while (time_before(jiffies, timeout)) { if (fifo_empty()) break; }看起来没什么问题,但实际排查发现,timeout是在一个循环外计算的,而循环内有一段阻塞操作耗时很长。由于中断被长时间关闭,jiffies在那段时间里没有递增,等到循环回来时,timeout和当前值的比较正常,但总等待时间远超预期。更严重的是,如果阻塞期间jiffies回绕过一次,而timeout又恰好落在回绕窗口附近,time_before()会判断错误,循环直接退化成忙等。
排查这类问题,我的经验是先在关键路径上加打印,记录jiffies和timeout的数值:
pr_info("now=%lu timeout=%lu diff=%ld\n", jiffies, timeout, (long)(timeout - jiffies));通过观察差值是否在合理范围内,能快速定位是判断逻辑写错了,还是timeout计算太早导致实际超时时间被延长。
5.4 如何在板子上观察 INITIAL_JIFFIES 的效果
想亲眼看到这个宏的效果,最简单的方法是写一个内核模块,在模块加载时打印jiffies的当前值和换算后的启动时间:
#include <linux/module.h> #include <linux/jiffies.h> static int __init my_init(void) { u64 now = get_jiffies_64(); pr_info("jiffies=%lu jiffies_64=%llu seconds=%llu\n", jiffies, now, now / HZ); return 0; } module_init(my_init); MODULE_LICENSE("GPL");如果系统刚启动不到 5 分钟,打印出的jiffies_64 / HZ就是 0 到 299 之间的一个数。超过 300 秒后,这个值会跳到 300,因为jiffies_64已经完整经过了从INITIAL_JIFFIES到回绕点的旅程。看过一次实测数据,对这个宏的理解会立刻深一层。
最后再分享一个体会
INITIAL_JIFFIES是我觉得内核源码里“最像彩蛋”的宏之一。它只有一行,几乎所有文档都没提过,但你真的去理解它时,会顺带把无符号回绕、启动时序、定时器实现这些概念全部串起来。我个人的建议是,读内核代码时遇到这种一眼看不懂的常量,不要直接跳过,花十分钟查一下它的来龙去脉,往往会有意想不到的收获。这个习惯帮我在排查驱动问题时少走了很多弯路,尤其是那些跟时间、超时、回绕相关的疑难杂症,提前知道底层的偏移设定,排查思路会清晰很多。