系统启动以来产生的节拍的总数——jiffies
一、 什么是 jiffy?
“Jiffy”一词本身,在工程与科学领域泛指一个短暂、不确定的时间间隔。在物理学中,它可能是光穿越一个原子核的时间;在电气工程中,它是一个交流电周期。而在操作系统的世界里,一个 jiffy 被明确定义为两次连续时钟中断之间的间隔。它成为了内核感知时间流逝的基本单位。
那么,如何度量系统运行了“多少个”这样的时间单位呢?答案就是全局变量 jiffies。你可以将它理解为一个巨大的、永不停止的秒表。自系统启动那一刻起,内核将其初始化为 0。此后,每一次时钟中断(即每一个“节拍”)发生,这个变量的值就精确地增加 1。如果内核设定每秒产生HZ次时钟中断(例如 100 或 1000),那么jiffies每秒的增加量就是HZ。于是,计算系统的运行时间(秒)变得异常简单:jiffies / HZ。
在代码中,它如此声明:
extern unsigned long volatile jiffies;
这个volatile关键字至关重要,它告诉编译器此变量可能在任何时刻被硬件(时钟中断)改变,禁止对其读写进行激进的优化。
二、 jiffies 的内部表示:一场兼容与未来的博弈
jiffies被声明为unsigned long。在 32 位系统上,它是 32 位;在 64 位系统上,则是 64 位。这引发了一个关键问题:溢出。
一个 32 位的无符号整数最大约为 43 亿。当时钟频率HZ=100时,它会在约 497 天后归零(溢出);若HZ=1000,这个时间将缩短至约 50 天。对于需要长期稳定运行的服务器,这是一个必须面对的挑战。而 64 位的jiffies,其溢出时间则漫长到可以视为永恒。
然而,内核开发者面临一个两难:为了性能与兼容海量现存内核代码,他们希望jiffies保持unsigned long类型;但为了从根本上避免溢出,又需要 64 位的宽度。
他们的解决方案充满巧思。内核实际定义了另一个变量:
extern u64 jiffies_64; // 一个真正的64位变量
然后,通过链接器脚本进行如下的操作”:
jiffies = jiffies_64;
这条指令使得符号jiffies实际上指向jiffies_64的低 32 位。其精妙之处在于:
- 对绝大多数代码透明:旧代码一如既往地读取
jiffies,获取的只是低 32 位,完全不影响其逻辑(因为它们通常只关心较短的时间间隔)。 - 为时间管理预留空间:内核核心的时间管理代码可以通过
get_jiffies_64()等函数访问完整的 64 位,从而在自身运算中避免溢出。
在不同的体系结构上,此机制的表现不同:
- 在 32 位系统上:
jiffies是jiffies_64的低 32 位“视图”。 - 在 64 位系统上:
jiffies与jiffies_64通常是同一个 64 位变量,无需转换。
三、 回绕的陷阱与安全的比较
即便有了 64 位后备,32 位的jiffies视图自身的溢出(回绕)问题,在涉及时间比较的代码中仍是危险的陷阱。
考虑一个经典的超时检查场景:
unsigned long timeout = jiffies + HZ/2; // 设定0.5秒后超时
// ... 执行一些工作 ...
if (timeout > jiffies) { // 判断是否超时
// 未超时
} else {
// 已超时
}
这段代码在jiffies未溢出时工作正常。但想象一下:在timeout被设定为一个很大的值(接近 32 位最大值)后,jiffies在极短时间内溢出,归零成为一个很小的值。此时,逻辑判断(timeout > jiffies)将意外成立(因为timeout值仍然巨大),程序错误地判断为“未超时”。
为了解决因回绕导致的比较错误,内核提供了四个至关重要的宏:
#define time_after(a, b) // 时间a是否在时间b之后?
#define time_before(a, b) // 时间a是否在时间b之前?
#define time_after_eq(a, b) // 时间a是否在时间b之后或相等?
#define time_before_eq(a, b) // 时间a是否在时间b之前或相等?
利用这些宏,之前的超时检查可以改写为安全的版本:
unsigned long timeout = jiffies + HZ/2;
// ... 执行一些工作 ...
if (time_before(jiffies, timeout)) { // 使用安全宏
// 未超时
} else {
// 已超时
}
现在,无论jiffies是否回绕,判断都能得到正确的结果。
留下评论