对 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)  


<think>好的,我现在需要解决用户的问题:如何在操作系统中获取内核时间并以毫秒为单位计算时间差。用户提到了引用中的内容,涉及系统时间、实时时间、jiffies、xtime以及LOSCFG_BASE_CORE_TICK_PER_SECOND的配置。首先,我得理清这些概念之间的关系,以及如何在实际操作中应用它们。 首先,根据引用[2],系统时间(jiffies)是相对时间,记录自启动以来的时钟滴答数,而实时时间(xtime)是绝对时间,通常以秒和纳秒表示。用户需要的是内核时间,可能指的是系统时间,也就是jiffies。不过,不同的操作系统可能有不同的实现,比如Linux和LiteOS可能有所区别。用户提供的引用[3]提到了LiteOS的配置,每秒Tick数为100,因此每个Tick是10毫秒。这可能影响如何将jiffies转换为毫秒。 接下来,用户需要获取两个时间点的时间差,单位为毫秒。步骤应该是先获取两个时间点的值,然后计算差值,再转换为毫秒。如果使用jiffies,需要考虑每个Tick对应的毫秒数。例如,引用[3]中提到每秒100个Tick,那么每个Tick是10毫秒,所以时间差为(jiffies2 - jiffies1) * 10毫秒。但如果系统配置不同,比如Tick频率更高,转换系数会变化,所以需要确认系统的Tick速率。 另外,引用[1]提到如何度量时间差,内核可能通过jiffies或xtime来实现。如果使用xtime,即实时时间,可以通过获取当前的秒和纳秒,将它们转换为毫秒,然后相减。例如,时间点1的秒数乘以1000加上纳秒除以1e6,得到总毫秒数,同样处理时间点2,然后相减得到差值。 在代码实现上,需要考虑如何安全地读取这些变量,因为jiffies可能会溢出,而xtime在读取时可能需要锁来确保数据一致性。比如,在Linux中,读取xtime可能需要使用seq锁,循环读取直到数据一致。在LiteOS或其他实时操作系统中,可能有不同的API来获取时间。 用户可能需要具体的代码示例,比如在Linux中使用jiffies或do_gettimeofday,或者在LiteOS中使用LOS_TickCountGet函数。例如,使用jiffies的话,需要注意jiffies是32位还是64位,以及HZ的值,防止溢出。例如,HZ=100时,每个jiffy是10ms,所以差值乘以10得到毫秒。如果HZ=1000,则每个jiffy是1ms。 总结步骤: 1. 确定操作系统型(如Linux或LiteOS)。 2. 根据系统选择合适的时间获取方法(jiffies或xtime)。 3. 获取两个时间点的值。 4. 计算差值并转换为毫秒,考虑Tick频率。 5. 处理可能的溢出或数据同步问题。 可能的错误点包括未考虑HZ的不同配置,或者未正确处理32位jiffies溢出问题。例如,当jiffies是32位且HZ=1000时,大约49.7天后会溢出,所以需要处理这种情况,使用时间比较的time_aftertime_before。 此外,用户可能需要知道如何在内核模块中实现这些,或者在用户空间使用系统调用。例如,用户空间可能需要使用clock_gettime()函数获取CLOCK_MONOTONIC时间,然后计算差值。不过用户的问题明确提到“内核时间”,所以可能需要内核模块的代码示例。 最后,结合用户提供的引用,特别是引用[3]中的LOSCFG_BASE_CORE_TICK_PER_SECOND设置为100,这可能是指LiteOS的环境,因此示例代码可能需要基于LiteOS的API,比如使用LOS_TickCountGet()来获取当前的Tick数,然后乘以每个Tick的毫秒数(如10ms)得到时间差。</think>### 获取内核时间并计算时间差的方法 #### 1. **理解内核时间的型** - **系统时间(jiffies)**:记录自系统启动以来的时钟滴答数(相对时间),适用于短时间间隔的度量[^2]。 - **实时时间(xtime)**:记录自1970年1月1日以来的秒和纳秒(绝对时间),适用于长时间跨度的计算[^2]。 #### 2. **通过jiffies计算时间差(适用于短间隔)** - **步骤**: 1. 获取当前jiffies值:`jiffies1 = jiffies;` 2. 经过一段时间后,再次获取:`jiffies2 = jiffies;` 3. 计算差值并转换为毫秒: ```c unsigned long diff_jiffies = jiffies2 - jiffies1; unsigned long diff_ms = diff_jiffies * (1000 / HZ); // HZ为系统每秒的Tick数 ``` - **示例**:若HZ=100(每秒100 Tick),则每Tick=10毫秒,差值为`diff_jiffies * 10`[^3]。 #### 3. **通过xtime计算时间差(适用于长间隔)** - **步骤**: 1. 获取当前xtime值(需处理数据一致性): ```c struct timespec64 now; ktime_get_real_ts64(&now); // 安全获取实时时间 ``` 2. 计算时间差(单位:毫秒): ```c unsigned long long start_ms = start_time.tv_sec * 1000 + start_time.tv_nsec / 1000000; unsigned long long end_ms = end_time.tv_sec * 1000 + end_time.tv_nsec / 1000000; unsigned long long diff_ms = end_ms - start_ms; ``` #### 4. **代码示例(基于Linux内核)** ```c // 使用jiffies(短时间) unsigned long start_jiffies = jiffies; // ...执行操作... unsigned long end_jiffies = jiffies; unsigned long diff_ms = (end_jiffies - start_jiffies) * (1000 / HZ); // 使用xtime(长时间) struct timespec64 start_time, end_time; ktime_get_real_ts64(&start_time); // ...执行操作... ktime_get_real_ts64(&end_time); unsigned long long diff_ms = (end_time.tv_sec - start_time.tv_sec) * 1000 + (end_time.tv_nsec - start_time.tv_nsec) / 1000000; ``` #### 5. **注意事项** - **Tick频率**:需确认系统的HZ值(如通过`CONFIG_HZ`配置),否则转换系数错误会导致时间差计算不准确。 - **溢出问题**:jiffies为32位时,若HZ=1000,约49.7天后会溢出,需使用`time_after`判断时间先后[^1]。 - **实时性**:xtime可能因NTP调整发生变化,若需要单调递增的时间,应使用`CLOCK_MONOTONIC`。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值