对 jiffies 溢出、回绕及 time_after 宏的理解
前言 #
时隔数年之后,打算重写这一篇文章,虽然这几年并没有机会继续深入了解 Linux 内核,但是陆续熟悉了 μC/OS、FreeRTOS 等实时操作系统,对 C 的理解也有长进,应该能写得更清楚。但是在开始正文之前,请读者们打开计算器,切换到程序员模式,方便查看二进制数😄。
正文 #
Linux 内核源码里用 jiffies 这个变量来记录系统启动以来所经历的 tick 数,timeout、delay、sleep 之类的实现会依赖于它。它的类型是 unsigned long
,其每毫秒自加 1,假设它是 u32(无符号32位),那么约 49.71 天后会溢出。题外话是,现在 64 位操作系统已是主流,所以 unsigned long
很可能要比 u32 大,但这并不会导致计算逻辑发生变化。
那么当这个数发生溢出回绕时,程序对时间的判断是不是会发生问题?我们知道一年到头不关机的服务器多的是。所以一个靠谱的操作系统,一定是在这个数产生溢出时也不会出现问题的,所以我们是不是要写几个 if else 来应对这种情况?答案是,并不需要,在 Linux 源码中有这样一个宏用来应对这一情况,而且相当简单:
#define time_after(a,b)\
(typecheck(unsigned long, a) &&\
typecheck(unsigned long, b) &&\
((long)(b)-(long)(a)<0) // 当 a 在 b 的后面(大于等于)时,此宏为 true
// timeout 为超时时间点,jiffies >= timeout 代表超时,宏结果为 true
time_after(jiffies, timeout);
简单说就是把无符号数强制转换成有符号数来计算。猛的一看,可能很难理解这个宏为什么能完美的解决这一问题。为了将问题简化,我们下面会用 u8 和 i8 来进行计算。
首先我们要明确一点,无论是用 i8 还是 u8 进行自加,从二进制的角度看都是从 0 加到 b11111111 然后再变回 0,不同的只是 i8 加到一半的时候符号会跳变罢了,因为最高位为符号位,理解困难的同学,请按计算器。
那么在 u8 和 i8 计算时,分别存在一个困惑点,就是 u8 在 0 附近,i8 在符号跳变附近进行计算时,分别会发生什么?
先说 i8,其最大的正数是 127, 127 + 1 = -128
,以二进制看就是 b01111111 变成了 b10000000,那么我们就以 126
和 -127
作为例子进行相互的计算,他们之间的数值差是 3
,下面请按计算器。
正向:
-127 - 126
= -253
其范围超出了 8 bits
= 0xFF03
转换为 i8,截去高位
= 0x03
= 3
反向:
126 - (-127)
= 253
= 0xFD
转换为 i8,有符号位
= -3
从结果来看,无论是正向还是反向计算,只要最后得到的结果转换为同样的 i8 类型,那么就能够得到正确的差值方向和个数。同理,两个 long
类型的数做差只要最后结果也转换为 long
类型,那么也能得到正确的结果。
于是我们可以得出结论,两个同尺寸的数做差,将结果转换为同尺寸的有符号数的话,其符号方向和数值大小是正确的,不受溢出的影响,但前提是正负差值不能大于该类型的正负极值。 以 u32 来说,一次性时间差的判断不得大于 24.86 天。这个问题很容易规避,只要将一次长延时切分成多次短延时就好了。
这也就是上面那个宏能够有效的原因,甚至我们可以干脆把自增的 jiffies 变量类型也改成 long
,然后就只需要对做差的结果做一次类型转换就好了。当然了,这还得考虑到是否影响它在其它代码中的使用。
我自己用的话可能会写成这样:
#define time_after(_a, _b) ((i32)((i32)(_b)-(i32)(_a)) < 0)
补充 #
额外补充无符号的情况。 u8 0xFE - 1 = 0xFD
这很简单,但是 1 - 0xFE
等于多少?
1 - 0xFE
= (1 - 2) - 0xFC
= 0xFF - 0xFC
= 3
可以看出无符号的运算结果无法算出负数,但是正向的计算结果是正确的,不受溢出影响。也就是说如果你的应用场景只需要算出正向的时间差,那么直接用无符号计算就行了,不会有任何问题。