Linux 下的 time_before/time_after 接口

1、引言

  最近在开发过程中遇到一个因 jiffies 使用不当导致系统卡死的问题。问题的根源在于:在延时或超时判断中直接比较 jiffies 的原始值,而未使用内核提供的安全宏(如 time_before)。由于 jiffies 是一个 unsigned long 类型的全局计数器,在 HZ=1000(即每毫秒递增一次)的系统上,大约每 49.7 天就会发生一次回绕(wrap-around)。若在回绕窗口附近进行裸数值比较,很容易因整数溢出导致条件判断失效,进而引发死循环或超时逻辑异常。

2、代码讲解

include\linux\jiffies.h

c 复制代码
/*
 *	These inlines deal with timer wrapping correctly. You are 
 *	strongly encouraged to use them
 *	1. Because people otherwise forget
 *	2. Because if the timer wrap changes in future you won't have to
 *	   alter your driver code.
 *
 * time_after(a,b) returns true if the time a is after time b.
 *
 * Do this with "<0" and ">=0" to only test the sign of the result. A
 * good compiler would generate better code (and a really good compiler
 * wouldn't care). Gcc is currently neither.
 */
#define time_after(a,b)		\
	(typecheck(unsigned long, a) && \
	 typecheck(unsigned long, b) && \
	 ((long)((b) - (a)) < 0))
#define time_before(a,b)	time_after(b,a)

代码片段:

c 复制代码
static int sunxi_mmc_reset_host(struct sunxi_mmc_host *host)
{
	unsigned long expire = jiffies + msecs_to_jiffies(250);
	u32 rval;

	mmc_writel(host, REG_GCTRL, SDXC_HARDWARE_RESET);
	do {
		rval = mmc_readl(host, REG_GCTRL);
	} while (time_before(jiffies, expire) && (rval & SDXC_HARDWARE_RESET));

	if (rval & SDXC_HARDWARE_RESET) {
		dev_err(mmc_dev(host->mmc), "fatal err reset timeout\n");
		return -EIO;
	}

	return 0;
}

为什么 time_before 是安全的呢?

2.1 无符号运算

C 标准中关于无符号数的运算有这样一个定义

A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.

               ------------------------------------------《ISO/IEC 9899:2011, Programming languages --- C, Section 6.2.5, paragraph 9.》

  翻译过来,其实就是:

  • 在 C 语言中,unsigned 整数的运算不会产生 UB(未定义行为)
  • 当结果超出它能表示的范围时,它会自动对模进行取余(wrap around)

  举个例子,对于 unsigned int 类型,如果结果超出其表示范围(如 UINT_MAX + 1),结果会自动对 UINT_MAX + 1 取模。不会发生溢出错误,行为是明确定义的(well-defined)。

类型 模 最大值
unsigned char 256 254
unsigned int 2³² 2³² - 1

取模运算公式:
x mod y=x−y×⌊x/y⌋x\bmod y = x - y \times \lfloor x / y \rfloorxmody=x−y×⌊x/y⌋

无符号运算的本质是 模 2ⁿ 的环(wrap around)。

32 位系统上:

cpp 复制代码
unsigned long 范围:0 ~ 2^32 - 1
模数:2^32

代入上面的取模公式:

0U−254U=−254 mod 232=−254−232×⌊−254/232⌋0U - 254U = -254\bmod 2^{32} = -254 - 2^{32} \times \lfloor -254 / 2^{32} \rfloor0U−254U=−254mod232=−254−232×⌊−254/232⌋

因此:

cpp 复制代码
0U - 254U = (2^32 - 254)

2.2 小结

time_before/time_after 的数学原理

宏内部做了:

  • (b - a) 使用 unsigned long 算术
    → 自动 wrap-around
  • 结果再强制转换为 long(有符号长整型)

如果 a 和 b 的时间差不超过 2³¹−1(有符号 long 的最大值),那么 (long)(b - a) 的符号足以判断先后顺序:

  • 如果 a 在 b 之后:数学差值 a−b 是正数
    • 如果 a - b < 231 - 1,(a - b) 的 unsigned 值落在 0 ~ 2^31-1,转成 long 是正数
  • 如果 a 在 b 之前:数学差值 a−b 是负数(可能 wrap 了 ):
    • 如果 |a-b| <= 232 - 231 + 1, (a - b) 的 unsigned 值落在 2^31 ~ 2^32-1,转成 long 是负数
    • 如果 232 - 231 + 1 < |a-b| , (a - b) 的 unsigned 值落在 0 ~ 2^31-1,转成 long 是正数

上面的 ∣a−b∣ 指的是数学意义上的时间差。以 sunxi_mmc_reset_host 为例,expire 的生成时刻与之后在 time_before() 中读取到的 jiffies 之间的真实时间差只要小于 231 - 1,就能保证 (long)(a-b) 的结果符号是正确的,从而保证 time_before() 返回的先后判断是可靠的。

两个时间点之间的间隔:

bash 复制代码
2^31 - 1 ticks

也就是约 24.8 天(100Hz 时)/ 49.7 天(HZ=100)/ 497 天(HZ=1000)这个范围远大于内核中绝大多数超时判断。所以可认为 time_before 接口可靠。

相关推荐
差不多的周周4 小时前
《骨架上下文:基于零镜头骨架的动作识别的骨架侧上下文提示学习》论文核心部分详解
学习
YoungStudyAI4 小时前
QA的下一个自动化阶段,可能不是再写更多脚本,而是设计测试Agent
运维·自动化
Century_Dragon5 小时前
从挂图到VR:汽修专业的一堂新能源原理课可以怎么上~
学习
筝筝ba5 小时前
openBMC功耗封顶GUI测试
服务器
分布式存储与RustFS5 小时前
纠删码到底吃掉了多少容量:EC:4、EC:8 的利用率与容错怎么算
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
遇见小修修6 小时前
3步解决打印机0x00000709报错 ,广州深圳程序员可自行排查
运维·打印机·维修·打印机报错·打印机运维·打印机故障自查
知识分享小能手6 小时前
C++ 学习教程,从入门到精通,C++对象和类 — 详细知识点总结(10)
开发语言·c++·学习
顺顺 尼6 小时前
linux 进程信号(上)
linux·c++·算法
niuTaylor6 小时前
飞腾 FT-2000/4 Ubuntu 内存卡 / NVMe SSD 系统制作完整指南
linux·服务器·人工智能·驱动开发·单片机
New农民工6 小时前
倒立摆训练秘籍:理解训练参数
学习