对 jiffies 溢出、回绕及 time_after 宏的理解

原文如下:

关于jiffies变量: 
    全局变量jiffies用来记录自启动以来产生的节拍的总数。系统启动时会将该变量初始化为0,此后,每当时钟中断产生时就会增加该变量的值。jiffies和另外一个变量息息相关:HZ。HZ是每秒系统产生的时钟中断次数,所以jiffies每秒增加的值也就是HZ;在x86体系结构中,内核版本在2.4以前的值为100,在2.6内核中被定义为1000。  jiffies的定义:
    extern unsigned long volatile jiffies;  //定义于<linux/jiffies.h>
    从定义可以看出,jiffies的类型为unsigned long,在32位体系结构上unsigned long是32位,在64位体系结构上是64位。 在32位体系结构上,在系统的HZ值为100的情况下,jiffies的回绕时间大约为500天左右,如果HZ为1000,那么回绕时间将只有50天左右。如果发生了回绕现象,对内核中直接利用jiffies来比较时间的代码将产生很不利的影响,比如在<<Linux Kernle Developmen>>一书中有一个例子可以说明这个问题:
    unsigned long timeout = jiffies + HZ/2; //0.5后超时
    /*执行一些任务*/
    ........
    /*然后检查时间是否过长*/
    if(timeout>jiffies){
           /*没有超时...*/
    }else{
           /*超时了....*/
    }
    在这个例子中,如果设置了timeout后发生了回绕,那么第一个判断条件将变为真,这与实际情况不符,尽管因为实际的时间比timeout要大,但因为jiffies发生了回绕变成了0,所以jiffies肯定小于timeout的值。  内核也专门针对这种情况提供了四个宏来帮助比较jiffies计数:
    #define  time_after(unknown,known)           ((long)(known) - (long)(unknown)<0)
    #define  time_before(unkonwn,known)         ((long)(unknown) - (long)(known)<0)
    #define  time_after_eq(unknown,known)      ((long)(unknown) - (long)(known)>=0)
    #define  time_before_eq(unknown,known)    ((long)(known) -(long)(unknown)>=0)
    这些宏看起来很奇妙,只是将计数值强制转换为long类型然后比较就能避免回绕带来的问题,这是为什么呢?这和计算机中数据存储的形式有关!!
    计算机中数据的存储是按照二进制补码的方式存储的,之所以采用补码的方式存储是因为这样可以简化运算规则和线路设计。另外一个相关的概念就是原码,原码采用一个最高位作为符号位,其余位是数据大小的二进制表示。 补码的定义是这样的:正数的补码即为原码,负数的补码为原码除符号位外其他各位取反再加1。举例如下:
    [+1]补码 = [+1]原码 = 0000 0001
    [- 1]补码 =  [- 1]原码取反+1 = 1111 1110 + 1 = 1111 1111
    而c语言中的数据类型相当于在代码和实际机器的存储之间的一个中间层,机器中存储的数据,如果按照不同的类型格式取读取就会得到不同的结果,才代码和实际存储之间,编译器充当了翻译者的角色。这是编译器能实现多种数据类型和强制类型转换的基础。
    有了这些基础后,就不难理解上述宏定义的巧妙之处了,为了便于说明,以下假设jiffies是单字节的无符号数,范围为0~255。假如jiffies开始为250,由于是无符号数据,那么它在机器中实际存储的补码为11111010,记为J1;timeout如果被设为252,实际存储为11111100;而过了一会jiffies发生回绕编变成了1,实际存储变为00000001,记为J2。 那么此时如果按照无符号数比较其大小关系,有: J1<timeout & J2 <timeout,这样的结果与实际的时间节拍统计是不符的,但是如果我们按照有符号数来比较会有什么结果呢?
    J1如果按照有符号数读取,首先从补码转换成原码:10000110,转换成十进制为-6;
    timeout按照有符号数读取,首先从补码转换成原码:10000100,转换成十进制为-4;
    J2按照有符号数读取,首先从补码转换成原码:00000001,转换成十进制为1;
    这样它们的大小关系为: J1<timeout<J2。 这与实际的节拍计数就吻合了,以上内核定义的几个宏就是通过这种方式巧妙解决jiffies回绕问题的

 

最近在慢慢的啃着 Linux 内核的相关源码,读到 jiffies 这里,这个东西和 windows 下 GetTickCount 获得的值是类似的,就是系统启动以来所经历的 tick 数(windows 下是一毫秒一 tick),神马 timeout、delay、sleep 之类的东西还有进程的调度等,凡是与时间有关的东西大多依赖于它。以前看书的时候总是读到类似这样的内容:这个值一般是32位的,会在0到2^32之间循环,约49.71天。

那时还很天真的想,自己一般一天关机一次,想来是够用了,现在再看,够用个毛啊,一年到头不关机的机器多的是啊。想来那些写操作系统的大牛是不会留下这么二的 BUG 的,果然,今天研究到了这里,在 Linux 内核中使用了一个这样的宏来解决这个问题:

#define time_after(a,b)\    

(typecheck(unsigned long,a) &&\   

typecheck(unsigned long,b) &&\   

((long)(b)-(long)(a)<0) //当 a 在 b 的后面(大于等于),此宏为真   

将无符号数转成有符号数计算,的确是可以解决自加溢出的问题,以 char 为例,unsigned char 的 255 比 0 大,但是转成 signed char 之后其值为 -1 比 0 小,于是 -1, 0, 1 ... 就形成了连续性的递增关系,解决了溢出时的问题(这里不明白的同学请随便找一本C语言书看看补码、反码那一块)。

结合程序来看,假设我需要设置一个 5 tick 的延时,那么 timeout = jiffies + 5;如果运气不好,恰巧这个时候 jiffies 等于254,那么 timeout  等于 254 + 5 = 3,若没有使用宏而直接判断 jiffies > timeout ,那么结果就不对了,本来在 5 个 tick 内 jiffies 都应该小于 timeout 才对的,但 254 直接就大于 3,于是延时失败了。而使用宏的话 254 当做 -2 ,于是 5 个 tick 过后它才会大于 3,符合预期。

但我写到这里,上面的内容基本上等同于废话 ^@^ ,因为那些并不是我读到这个部分源码时所纠结的问题,我纠结的是,当 jiffies 从 127 加到 128 ,由正数转为负数的回绕产生时,time_after 是如何保证比较的结果的正确性的?为此,我自己写了个简化版的宏来做试验:

#define time_after(a,b) ((char)(b)-(char)(a)<0)    

//这个宏在 VC 下是不对的,后面解释   

 

经过一番研究以及阅读别人的帖子,我终于了解了它的原理,在此与大家分享,希望能够对他人有所帮助。

我们假设当前的 jiffies 值为 127 其 5 个 tick 之后的值为 132 ,以有符号来解释该数据则 timeout = -124,那么 jiffies 如何可能会小于 timeout 呢?timeout - jiffies > 0 又是怎么成立的呢?请听下回分解!

别砸显示器,开个玩笑。-124 - 127 = -251 是负数,貌似条件无法成立了,但它就能成立,因为 -251 在内存里的存储是 0xFF05,它超出了一个 byte 的范围,于是将其高位截去剩下 0x05,它是正数,于是 timeout - jiffies > 0 就成立了,坑爹啊!!!

但这个写法有一个小小的缺点,就是延时 tick 不能大于 127,否则计算就不正确了,换成 32 位的情况就是延时 tick 不能大于 2^31-1 想来也没有哪个程序会一次性 delay 25 天吧,所以这一套宏便能够胜任了。

顺便一提,我比较倒霉,因为我是在 VC 的环境下测试的这个宏,结果怎么都不对,原因是在于其运算时隐式转换的规则有细微的不同,它得出结果后并没有将数据截去高位,而是应该升级成了 int 型,于是负号被保留了下来,判断出错。改为如下即可:

#define time_after(a,b) ((char)((char)(b)-(char)(a))<0)  

 

 

posted on 2018-04-28 14:59  AlanTu  阅读(1628)  评论(0编辑  收藏  举报

导航