linux内核分析笔记----定时器和时间管理

 

 

      在这一次里,主要讲讲和时间相关的东西,这个我们都比较熟悉,我就直接如主题。

      首先要明白两个概念:系统定时器和动态定时器。周期性产生的事件都是有系统定时器驱动的,这里的系统定时器是一种可编程硬件芯片,它能以固定频率产生中断。该中断就是定时器中断,它所对应的中断处理程序负责更新系统时间,也负责执行需要周期行运行的任务。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。动态定时器是用来推迟执行程序的工具。内核可以动态创建或销毁动态定时器。

       内核必须在硬件的帮助下才能计算和管理时间。硬件为内核提供了一个系统定时器用以计算流逝的时间,该时钟在内核中可看成是一个电子时间资源。系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定称为节拍率(tick rate).当时钟中断发生时,内核就通过一种特殊的中断处理程序对其进行处理。系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ.在系统启动时按照HZ值对硬件进行设置。体系结构不一样,HZ的值也不同,定义在asm/param.h中。刚提到的节拍率就是这个意思。周期是1/HZ秒。最后要说明的是这个HZ值在编写内核代码时,不是固定不变的,而是可调的。当然,对于操作系统而言,也并不是一定要这个固定的时钟中断。实际上,内核可以使用动态编程定时器操作挂起事件。这里就不多说了。

       在linux内核里,有一个叫jiffies的变量(定义在linux/jiffies)记录了自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于HZ,所以jiffies一秒内增加的值也就为HZ.系统运行时间以秒为单位计算,就等于jiffes/HZ.它作为在计算机表示的变量,就总存在大小,当这个变量增加到超出它的表示上限时,就要回绕到0.这个回绕看起来很简单,但实际上还是给我们编程造成了很大的麻烦,比如边界条件判断时。幸好,内核提供了四个宏来帮助比较节拍计数,这些宏定义在linux/jiffies.h可以很好的处理节拍回绕的情况:

      1   说明:unknown参数通常是jiffies,known参数是需要对比的值。

       如果改变内核中的HZ的值则会给用户空间中某些程序造成异常结果,这是因为内核是以节拍数/秒的形式给用户空间导出这个值的,在这个接口稳定了很长一段时间后,应用程序便逐渐依赖于这个特定的HZ的值了。所以如果在内核中更改了HZ的定义值,就打破了用户空间的常量关系----用户空间并不知道这个新的HZ的值。为了解决这个问题,内核必须更改所有导出的jiffies的值。内核定义了USER_HZ来代表用户空间看到的HZ值。内核可以使用宏jiffies_to_clock_t()将一个由HZ表示的节拍计数转换成一个由USER_HZ表示的节拍数。改宏的用法取决于USER_HZ是否为HZ的整数倍或相反。当是整数倍时,宏的形式相当简单:

?
1
#define jiffies_to_clock_t(x) ((x)/(HZ/USER_HZ));

       如果不是整数倍关系,那么该宏就得用更为复杂的算法了。同样的,如果是64位系统,内核使用函数jiffies_64_to_clock()将64位的jiffies值的单位从HZ转换为USER_HZ.

       体系结构提供了两种设备进行计时:系统定时器和实时时钟。系统定时器提供一种周期性触发中断机制。实时时钟(RTC)是用来持久存储系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保护系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中,实时时钟最主要的作用是在启动时初始化xtime变量。

       有了上面的概念基础,下面就分析时钟中断处理程序。它分为两个部分:体系结构相关部分和体系结构无关部分。相关的部分作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应地运行。执行的工作如下:

1.获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。
2.需要时应答或重新设置系统时钟。
3.周期性地使用墙上时间更新实时时钟。
4.调用体系结构无关的时间例程:do_timer().
中断服务程序主要通过调用与体系结构无关的例程do_timer()执行下面的工作:
1.给jiffies_64变量加1.
2.更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
3.执行已经到期的动态定时器.
4.执行scheduler_tick()函数.
5.更新墙上时间,该时间存放在xtime变量中.
6.计算平均负载值.

       do_timer看起来还是很简单的,应为它的主要工作就是完成上面的框架,具体的让其它函数做就好了:

?
1
2
3
4
5
6
void do_timer( struct pt_regs *regs)
{
     jiffies_64++;
     update_process_times(user_mode(regs));
     update_times();
}

       上述user_mode()宏查询处理器寄存器regs的状态,如果时钟中断发生在用户空间,它返回1;如果发生在内核模式,则返回0.update_process_times()函数根据时钟中断产生的位置,对用户或对系统进行相应的时间更新:

?
1
2
3
4
5
6
7
8
9
void update_process_times( int user_tick)
{
     struct task_struct *p=current;
     int cpu=smp_processor_id();
     int system =user_tick^1;
     updata_one_process(p,user_tick, system ,cpu);
     run_local_timers();
     scheduler_tick(user_tick, system );
}

       update_one_process()函数的作用是更新进程时间。它的实现是相当细致的。但注意,因为使用了XOR操作,所以user_tick和system两个变量只要其中有一个为1,则另外一个就必须为0,updates_one_process()函数可以通过判断分支,将user_tick和system加到进程相应的计数上:

?
1
2
p->utime = user;
p->stime = system ;

       上述操作将适当的计数值增加1,而另外一个值保持不变。也许你已经发现了,这样做意味着内核对进程时间计数时,是根据中断发生时处理器所处的模式进行分类统计的,它把上一个tick全部算给进程。但是事实上进程在上一个节拍器间可能多次进入和退出内核模式,而在在上一个节拍期间,该进程也不一定是唯一一个运行进程,但是这没办法。接下来的run_lock_times() 函数标记了一个软中断去处理所有到期的定时器。最后,scheduler_tick()函数负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志,在SMP机器中中,该函数还要负责平衡每个处理器上的运行队列。当update_process_times()函数返回时,do_timer()函数接着会调用update_times()更新墙上时间。

?
1
2
3
4
5
6
7
8
9
10
void update_times( void )
{
     unsigned long ticks;
     if (ticks){
         wall_jiffies += ticks;
         update_wall_time(ticks);
     }
     last_time_offset = 0;
     calc_load(ticks);
}

       这里的ticks记录最近一次更新后新产生的节拍数。通常情况下ticks显然应该等于1.但是时钟中断也有可能丢失,因而节拍也会丢失。在中断长时间被禁止的情况下,就会出现这种现象(这种情况并不常见,往往是个BUG).wall_jiffies值随后被加上ticks----所以此刻wall_jiffies值就等于更新的墙上时间的更新值jiffies----接着调用update_wall_time()函数更新xtime,最后由calc_load()执行。do_timer()函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。以上的工作每1/HZ都要发生一次。

       刚前边说的墙上时间就是我们常说的实际时间,指变量xtime,由结构体timespec定义(kernel/timer.c),如下:

?
1
2
3
4
struct  timespec{
     time_t tv_sec;  //秒,存放自1970年7月1日(UTC)以来经过的时间,1970年7月1日称为纪元
     long tv_nsec;   //纳秒,记录自上一秒开始经过的纳秒数
}

读写这个xtime变量需要xtime_lock锁,该锁是一个顺序锁(seqlock).关于内核读写就不说了,注意适当加解锁就好。回到用户空间,从用户空间取得墙上时间的主要接口是gettimeofday(),在内核中对应系统调用为sys_gettimeofday():

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
asmlinkage long sys_gettimeofday( struct timeval __user *tv, struct timezone __user *tz)
{
          if (likely(tv != NULL)) {
                  struct timeval ktv;
                  do_gettimeofday(&ktv);
                  if (copy_to_user(tv, &ktv, sizeof (ktv)))
                          return -EFAULT;
          }
          if (unlikely(tz != NULL)) {
                  if (copy_to_user(tz, &sys_tz, sizeof (sys_tz)))
                          return -EFAULT;
          }
          return 0;
}

       分析上面的函数发现,问题就集中在tv上。当tv非空,就调用do_gettimeofday(),它主要完成循环读取xtime的操作。如果tz参数为空,该函数将把系统时区(存放在sys_tz中)返回用户。如果给用户空间拷贝墙上时间或时区发生错误,该函数返回-EFAULT;如果成功,则返回0.另外,内核提供的time系统调用,几乎被gettimeofday()完全取代。C库函数提供的一些墙上时间相关的库调用如ftime和ctime。系统的settimeofday()是用来设置当前时间,它需要具有CAP_SYS_TIME权限。除了更新xtime时间以外,内核不会像用户空间程序那样频繁使用xtime。但也需要注意在文件系统的实现代码中存放访问时间戳时需要使用xtime。

      上面说完了有关硬时钟,下面开始新的话题,是关于定时器的(也称动态定时器或内核定时器)。定时器并不周期执行,它在超时后就自行销毁。定义器由定义在linux/timer.h中的time_list表示,如下:

?
1
2
3
4
5
6
7
8
9
struct timer_list {
          struct list_head entry;
          unsigned long expires; 
          spinlock_t lock;
          unsigned long magic; 
          void (*function)(unsigned long );
          unsigned long data; 
          struct tvec_t_base_s *base;
};

      内核提供了一组与定时器相关的用来简化管理定时器的操作。所有这些接口都声明在文件linux/timer.h中,大多数接口在文件kernel/timer.c中获得实现。有了这些接口,我们要做的事情就很简单了:

1.创建定时器:struct timer_list my_timer;

2.初始化定时器:init_timer(&my_timer);

3.根据需要,设置定时器了:

            my_timer.expires = jiffies + delay;

            my_timer.data = 0;

            my_timer.function = my_function;

4.激活定时器:add_timer(&my_timer);

      经过上面的几步,定时器就可以开始工作了。然而,一般来说,定时器都在超时后马上就会执行,但是也有可能被推迟到下一时钟节拍时才能运行,所以不能使用它来实现硬实时。如果修改定时器,使用mod_timer(&my_timer,jiffies+new_delay)来修改已经激活的定时器时间。它也可以操作那些已经初始化,但还没有被激活的定时器,如果定时器未被激活,mod_timer会激活它。如果第啊哟个定时器时未被激活,该函数返回0;否则返回1。但不论哪种情况,一旦从mod_timer函数返回,定时器都将被激活而且设置了新的定时值。当然你也可以在超市前删除定时器用:del_timer(&my_timer);另外需要注意的是在多处理器上定时器中断可能已经在其它机器上运行了,这是就需要等待可能在其它处理器上运行的定时器处理程序都退出后再删除该定时器。这是就要使用del_timer_sync()函数执行删除工作。这个函数参数和上面一个一样,只是不能在中断上下文中使用而已。定时器是独立与当前代码的,这意味着可能存在竞争条件,这个就要特别小心,从这个意义上讲后者删除比前者更加安全。

      内核在时钟中断发生后执行定时器,定时器作为软件中断在下半部上下文中执行。具体来说就是时钟中断处理程序会执行update_process_timers()函数,该函数随即调用run_local_timers()函数:

?
1
2
3
4
void run_local_timers( void )
{
     raise_softirq(TIMER_SOFTIRQ);
}

      这个函数处理软中断TIEMR_SOFTIRQ,从而在当前处理器上运行所有的超时定时器。所有定时器都以链表的形式组织起来,但如果单纯的链表结构显然影响性能,因为每次都要顺序的的查找调整,这个时候,内核定时器按它们的超时时间将他们分为5组,当定时器超时时间接近时,定时器将随组一起下移。采用这种方法可以减少搜素超时定时器所带来的负担。

下一话题,内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外还提供了许多延迟的方法来处理各种延迟请求。下面就来总结一下:

1.忙等待(也叫忙循环):通常是最不理想的方法,因为处理器被白白占用旋转而无法做别的事情。该方法仅仅在想要延迟的时间是节拍的整数倍或者精确率要求不高时才可以使用。实现起来还是挺简单的,就是在循环中不断旋转直到希望的时钟节拍数耗尽。比如:

?
1
2
3
unsigned long delay = jiffies+10;   //10个节拍
while (time_before(jiffies,delay))
     cond_resched();

      缺点很明显,更好的方法是在代码等待时,允许内核重新调度执行其他任务,如下:

?
1
2
3
unsigned long delay = jiffies+10;   //10个节拍
while (time_before(jiffies,delay))
     cond_resched();

      cond_resched()函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后才能生效。换句话说,就是系统中存在更重要的任务需要运行。再由于该方法需要调用调度程序,所以它不能在中断上下文中使用----只能在进程上下文中使用。事实上,所有延迟方法在进程上下文中使用,因为中断处理程序都应该尽可能快的执行。另外,延迟执行不管在哪种情况下都不应该在持有锁时或者禁止中断时发生。

      至于说那些需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间很精确,这种情况多发生在和硬件同步时,也就是说需要短暂等待某个动作的完成----等待时间往往小于1ms,所以不可能使用像前面例子中那种基于jiffies的延迟方法。这时,就可以使用在linux/delay.h中定义的两个函数,它们不使用,这两个函数可以处理微秒和毫秒级别的延迟的时间,如下所示:

?
1
2
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

      前者是依靠执行次数循环来达到延迟效果的,而mdelay()函数又是通过udelay()函数实现的。因为内核知道处理器在一秒内能执行多少次循环,所以udelay()函数仅仅需要根据指定的延迟时间在1秒中占的比例,就能决定需要进行多少次循环就能达到需要的推迟时间。udelay()函数仅能在要求的延迟时间很短的情况下执行,而在高速机器中时间很长的延迟会造成溢出,经验表明,不要试图在延迟超过1ms的情况下使用这个函数。这两个函数其实和忙等待一样,如果不是非常必要,还是不要用了算了。

      前边说的有点害怕,那咋办呢?其实更理想的延迟执行方法是使用schedule_timeout()函数,该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。但该方法也不能保证睡眠时间正好等于指定的延迟时间----只能尽量是睡眠时间接近指定的延迟时间。当指定的时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列,如下:

?
1
2
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(s*HZ);

      唯一的参数是延迟的相对时间,单位是jiffies,上例中将相应的任务推入可中断睡眠队列,睡眠s秒。在调用函数schedule_timeout之前,不要要将任务设置成可中断或不和中断的一种,否则任务不会休眠。这个函数需要调用调度程序,所以调用它的代码必须保证能够睡眠,简而言之,调用代码必须处于进程上下文中,并且不能持有锁。有关这个函数的实现细节,可以看下源码,还是相当简单的。接下来就是当定时器超时,process_timeout()函数被调用:

?
1
2
3
4
void process_timeout(unsigned long data)
{
     wake_up_process((task_t *)data);
}

      该函数将任务置为TASK_RUNNING状态,然后哦将其放入运行队列。当任务重新被调度时,将返回代码进入睡眠前的位置继续执行(正好在调用schedule()后)。如果任务提前被唤醒(比如收到信号),那么定时器被销毁,process_timeout()函数返回剩余的时间。

      最后,在进程调度那一节我们说过,进程上下文的代码为了等待特定时间发生,可以将自己放入等待队列。但是,等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定时间到期----就看谁来得更快。这种情况下,代码可以简单的使用scedule_timeout()函数代替schedule()函数,这样一来,当希望指定时间到期后,任务都会被唤醒,当然,代码需要检查被唤醒的原因----有可能是被事件唤醒,也有可能是因为延迟的时间到期,还可能是因为接收到了信号----然后执行相应的操作。

 

 

 
  
为何time_before 起作用。
Title: jiffies溢出与时间先后比较

1. 概述
在Linux内核中,TCP/IP协议栈在很多用到时间比较的地方都使用了jiffies?本文介绍了什么是jiffies,jiffies溢出可能造成的问题,使用time_after等宏来正确地比较时间及其背后的原理。

2. jiffies简介

2.1 时钟中断
在Linux内核中,TCP/IP协议栈在很多用到时间比较的地方都使用了jiffies。
那么jiffies是什么呢?我们知道,操作系统应该能够在将来某个时刻准时调度某个任务,所以需要一种能保证任务准时调度运行的机制。希望支持每种操作系统的微处理器必须包含一个可周期性中断它的可编程间隔定时器。这个周期性中断被称为系统时钟滴答(或system timer),它象节拍器一样来组织系统任务,也称为时钟中断。
时钟中断的发生频率设定为HZ,HZ是一个与体系结构无关的常数,在文件<linux/param.h>中定义。至少从2.0版到 2.1.43版,Alpha平台上Linux定义HZ的值为1024,而其他平台上定义为100。时钟中断对操作系统是非常重要的,当时钟中断发生时,将周期性地执行一些功能,例如:
. 更新系统的uptime
. 更新time of day
. 检查当前任务的时间片是否用光,如果是则需要重新调度任务
. 执行到期的dynamic timer
. 更新系统资源使用统计和任务所用的时间统计

2.2 jiffies及其溢出
全局变量jiffies取值为自操作系统启动以来的时钟滴答的数目,在头文件<linux/sched.h>中定义,数据类型为 unsigned long volatile (32位无符号长整型)。关于jiffies为什么要采用volatile来限定,可参考《关于volatile和jiffies.txt》。
jiffies转换为秒可采用公式:(jiffies/HZ)计算,将秒转换为jiffies可采用公式:(seconds*HZ)计算。
当时钟中断发生时,jiffies值就加1。因此连续累加一年又四个多月后就会溢出(假定HZ=100,1个jiffies等于1/100 秒,jiffies可记录的最大秒数为(2^32 -1)/100=42949672.95秒,约合497天或1.38年),即当取值到达最大值时继续加1,就变为了0。
在Vxworks操作系统中,定义HZ的值为60,因此连续累加两年又三个多月后也将溢出(jiffies可记录的最大秒数为约合2.27 年)。如果在Vxworks操作系统上的应用程序对jiffies的溢出没有加以充分考虑,那么在连续运行两年又三个多月后,这些应用程序还能够稳定运行吗?
下面我们来考虑jiffies的溢出,我们将从以下几个方面来阐述:
. 无符号整型溢出的具体过程
. jiffies溢出造成程序逻辑出错
. Linux内核如何来防止jiffies溢出
. time_after等比较时间先/后的宏背后的原理
. 代码中使用time_after等比较时间先/后的宏

3. 无符号整型溢出的具体过程
我们首先来看看无符号长整型(unsigned long)溢出的具体过程。实际上,无符号整型的溢出过程都很类似。为了更具体地描述无符号长整型溢出的过程,我们以8位无符号整型为例来加以说明。
8位无符号整型从0开始持续增长,当增长到最大值255时,继续加1将变为0,然后该过程周而复始:
0, 1, 2, ..., 253, 254, 255,
0, 1, 2, ..., 253, 254, 255,
...

4. jiffies溢出造成程序逻辑出错
下面,通过一个例子来看jiffies的溢出。
例4-1:jiffies溢出造成程序逻辑出错
unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */

/* do some work ... */
do_somework();

/* then see whether we took too long */
if (timeout > jiffies) {
/* we did not time out, call no_timeout_handler() ... */
no_timeout_handler();

} else {
/* we timed out, call timeout_handler() ... */
timeout_handler();
}
本例的目的是从当前时间起,如果在0.5秒内执行完do_somework(),则调用no_timeout_handler()。如果在0.5秒后执行完do_somework(),则调用timeout_handler()。
我们来看看本例中一种可能的溢出情况,即在设置timeout并执行do_somework()后,jiffies值溢出,取值为0。设在设置 timeout后,timeout的值临近无符号长整型的最大值,即小于2^32-1。设执行do_somework()花费了1秒,那么代码应当调用 timeout_handler()。但是当jiffies值溢出取值为0后,条件timeout > jiffies成立,jiffies值(等于0)小于timeout(临近但小于2^32-1),尽管从逻辑上讲jiffies值要比timeout大。但最终代码调用的是no_timeout_handler(),而不是timeout_handler()。

5. Linux内核如何来防止jiffies溢出
Linux内核中提供了以下四个宏,可有效地解决由于jiffies溢出而造成程序逻辑出错的情况。下面是从Linux Kernel 2.6.7版本中摘取出来的代码:
/*
* 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) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)

#define time_after_eq(a,b)
(typecheck(unsigned long, a) &&
typecheck(unsigned long, b) &&
((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)

在宏time_after中,首先确保两个输入参数a和b的数据类型为unsigned long,然后才执行实际的比较。这是以后编码中应当注意的地方。

6. time_after等比较时间先后的宏背后的原理
那么,上述time_after等比较时间先/后的宏为什么能够解决jiffies溢出造成的错误情况呢?
我们仍然以8位无符号整型(unsigned char)为例来加以说明。仿照上面的time_after宏,我们可以给出简化的8位无符号整型对应的after宏:
#define uc_after(a, b) ((char)(b) - (char)(a) < 0)

设a和b的数据类型为unsigned char,b为临近8位无符号整型最大值附近的一个固定值254,下面给出随着a(设其初始值为254)变化而得到的计算值:
a     b     (char)(b) - (char)(a)
254     254         0
255             - 1
0             - 2
1             - 3
...
124             -126
125             -127
126             -128
127             127
128             126
...
252             2
253             1






从上面的计算可以看出,设定b不变,随着a(设其初始值为254)不断增长1,a的取值变化为:
254, 255, (一次产生溢出)
0, 1, ..., 124, 125, 126, 127, 126, ..., 253, 254, 255, (二次产生溢出)
0, 1, ...
...

而(char)(b) - (char)(a)的变化为:
0, -1,
-2, -3, ..., -126, -127, -128, 127, 126, ..., 1, 0, -1,
-2, -3, ...
...

从上面的详细过程可以看出,当a取值为254,255, 接着在(一次产生溢出)之后变为0,然后增长到127之前,uc_after(a,b)的结果都显示a是在b之后,这也与我们的预期相符。但在a取值为 127之后,uc_after(a,b)的结果却显示a是在b之前。
从上面的运算过程可以得出以下结论:
使用uc_after(a,b)宏来计算两个8位无符号整型a和b之间的大小(或先/后,before/after),那么a和b的取值应当满足以下限定条件:
. 两个值之间相差从逻辑值来讲应小于有符号整型的最大值。
. 对于8位无符号整型,两个值之间相差从逻辑值来讲应小于128。
从上面可以类推出以下结论:
对于time_after等比较jiffies先/后的宏,两个值的取值应当满足以下限定条件:
两个值之间相差从逻辑值来讲应小于有符号整型的最大值。
对于32位无符号整型,两个值之间相差从逻辑值来讲应小于2147483647。
对于HZ=100,那么两个时间值之间相差不应当超过2147483647/100秒 = 0.69年 = 248.5天。对于HZ=60,那么两个时间值之间相差不应当超过2147483647/60秒 = 1.135年。在实际代码应用中,需要比较先/后的两个时间值之间一般都相差很小,范围大致在1秒~1天左右,所以以上time_after等比较时间先 /后的宏完全可以放心地用于实际的代码中。

7. 代码中使用time_after等比较时间先/后的宏
下面代码是针对例4-1修改后的正确代码:
例7-1:在例4-1基础上使用time_before宏后的正确代码
unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */

/* do some work ... */
do_somework();

/* then see whether we took too long */
if (time_before(jiffies, timeout)) {
/* we did not time out, call no_timeout_handler() ... */
no_timeout_handler();

} else {
/* we timed out, call timeout_handler() ... */
timeout_handler();
}

8. 结论
系统中采用jiffies来计算时间,但由于jiffies溢出可能造成时间比较的错误,因而强烈建议在编码中使用time_after等宏来比较时间先后关系,这些宏可以放心使用。

9. 参考资料
[1] 在《Linux Kernel Development, Second Edition》Chapter 10中给出了与时间相关的HZ、jiffies、延迟、各种timer等更全面,更精彩而有趣的讨论。
[2]《关于volatile和jiffies》,可见http://ggnm.coku.com/。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值