linux 内核定时器 timer_list详解 http://www.linux-cn.com/html/linux/kernel/20070412/1886.shtml http://lin

linux 内核定时器 timer_list详解http://www.linux-cn.com/html/linux/kernel/20070412/1886.shtml http://linux.ccidnet.com/art/741/20061116/952655_1.htmlLinux内核2.4版中去掉了老版本内核中的静态定时器机制,而只留下动态定时器。相应地在timer_bh()函数中也不再通过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。

7.6.1 Linux内核对定时器的描述Linux在include/linux/timer.h头文件中定义了数据结构timer_list来描述一个内核定时器: 

struct timer_list { struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); };

各数据成员的含义如下:

(1)双向链表元素list:用来将多个定时器连接成一条双向循环队列。

(2)expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。当一个定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。

(3)函数指针function:指向一个可执行函数。当定时器到期时,内核就执行function所指定的函数。而data域则被内核用作function函数的调用参数。内核函数init_timer()用来初始化一个定时器。实际上,这个初始化函数仅仅将结构中的list成员初始化为空。如下所示(include/linux/timer.h):static inline void init_timer(struct timer_list * timer) { timer->list.next = timer->list.prev = NULL; }由于定时器通常被连接在一个双向循环队列中等待执行(此时我们说定时器处于pending状态)。因此函数time_pending()就可以用list成员是否为空来判断一个定时器是否处于pending状态。如下所示 (include/linux/timer.h): static inline int timer_pending (const struct timer_list * timer) { return timer->list.next != NULL; }时间比较操作在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间关系比较操作宏。这里我们说时刻a在时刻b之后,就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏(include/linux/timer.h): #define time_after(a,b) ((long)(b) - (long)(a) < 0) #define time_before(a,b) time_after(b,a) #define time_after_eq(a,b) ((long)(a) - (long)(b) >= 0) #define time_before_eq(a,b) time_after_eq(b,a) 7.6.2 动态内核定时器机制的原理Linux是怎样为其内核定时器机制提供动态扩展能力的呢?其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双向循环定时器队列(对列中的每一个元素都是一个timer_list结构):对列中的所有定时器都在同一个时刻到期,也即对列中的每一个timer_list结构都具有相同的expires值。显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中,这个时间差值的最大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指针,这显然是不现实的。另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!时间间隔是以滴答次数为计数单位的)。基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),则Linux采用了下列思想来实现其动态内核定时器机制:对于那些0≤interval≤255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256个定时器向量。而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。所谓“松散的定时器向量语义”就是指:各定时器的expires值可以互不相同的一个定时器队列。具体的组织方案可以分为两大部分:

(1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义在kernel/timer.c文件中,如下述代码段所示: /* * Event timer code */ #define TVN_BITS 6 #define TVR_BITS 8 #define TVN_SIZE (1 << TVN_BITS) #define TVR_SIZE (1 << TVR_BITS) #define TVN_MASK (TVN_SIZE - 1) #define TVR_MASK (TVR_SIZE - 1) struct timer_vec { int index; struct list_head vec[TVN_SIZE]; }; struct timer_vec_root { int index; struct list_head vec[TVR_SIZE]; }; static struct timer_vec tv5; static struct timer_vec tv4; static struct timer_vec tv3; static struct timer_vec tv2; static struct timer_vec_root tv1; static struct timer_vec * const tvecs[] = { (struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5 }; #define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0])) 基于数据结构timer_vec_root,Linux定义了一个全局变量tv1,以表示内核所关心的前256个定时器向量。这样内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。而tv1的index字段则指定当前正在扫描定时器向量数组tv1.vec[256]中的哪一个定时器向量,也即该数组的索引,其初值为0,最大值为255(以256为模)。每个时钟节拍时index字段都会加1。显然,index字段所指定的定时器向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vec[index+k]则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当index值又重新变为0时,就意味着内核已经扫描了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。

(2)而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值的不同而不同。显然interval值越小,定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别对待。通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。内核规定,对于那些满足条件:0x100≤interval≤0x3fff的定时器,只要表达式(interval>>8)具有相同值的定时器都将被组织在同一个松散定时器向量中。因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需要26=64个松散定时器向量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。基于数据结构timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如上述代码段所示。对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4全局变量来表示这64个松散定时器向量。对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5全局变量来表示这64个松散定时器向量。最后,为了引用方便,Linux定义了一个指针数组tvecs[],来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。

7.6.3 

内核动态定时器机制的实现在内核动态定时器机制的实现中,有三个操作时非常重要的:

(1)将一个定时器插入到它应该所处的定时器向量中。

(2)定时器的迁移,也即将一个定时器从它原来所处的定时器向量迁移到另一个定时器向量中。

(3)扫描并执行当前已经到期的定时器。7.6.3.1 动态定时器机制的初始化函数init_timervecs()实现对动态定时器机制的初始化。该函数仅被sched_init()初始化例程所调用。动态定时器机制初始化过程的主要任务就是将tv1、tv2、…、tv5这5个结构变量中的定时器向量指针数组vec[]初始化为NULL。如下所示(kernel/timer.c): void init_timervecs (void) { int i; for (i = 0; i < TVN_SIZE; i++) { INIT_LIST_HEAD(tv5.vec + i); INIT_LIST_HEAD(tv4.vec + i); INIT_LIST_HEAD(tv3.vec + i); INIT_LIST_HEAD(tv2.vec + i); } for (i = 0; i < TVR_SIZE; i++) INIT_LIST_HEAD(tv1.vec + i); }上述函数中的宏TVN_SIZE是指timer_vec结构类型中的定时器向量指针数组vec[]的大小,值为64。宏TVR_SIZE是指timer_vec_root结构类型中的定时器向量数组vec[]的大小,值为256。 7.6.3.2 动态定时器的时钟滴答基准timer_jiffies由于动态定时器是在时钟中断的Bottom Half中被执行的,而从TIMER_BH向量被激活到其timer_bh()函数真正执行这段时间内可能会有几次时钟中断发生。因此内核必须记住上一次运行定时器机制是什么时候,也即内核必须保存上一次运行定时器机制时的jiffies值。为此,Linux在kernel/timer.c文件中定义了全局变量timer_jiffies来表示上一次运行定时器机制时的jiffies值。该变量的定义如下所示: static unsigned long timer_jiffies;


7.6.3.3 

对内核动态定时器链表的保护由于内核动态定时器链表是一种系统全局共享资源,为了实现对它的互斥访问,Linux定义了专门的自旋锁timerlist_lock来保护。任何想要访问动态定时器链表的代码段都首先必须先持有该自旋锁,并且在访问结束后释放该自旋锁。其定义如下(kernel/timer.c): /* Initialize both explicitly - let's try to have them in the same cache line */ spinlock_t timerlist_lock = SPIN_LOCK_UNLOCKED; 


7.6.3.4 

将一个定时器插入到链表中函 数add_timer()用来将参数timer指针所指向的定时器插入到一个合适的定时器链表中。它首先调用timer_pending()函数判断所指 定的定时器是否已经位于在某个定时器向量中等待执行。如果是,则不进行任何操作,只是打印一条内核告警信息就返回了;如果不是,则调用 internal_add_timer()函数完成实际的插入操作。其源码如下(kernel/timer.c): void add_timer(struct timer_list *timer) { unsigned long flags; spin_lock_irqsave(&timerlist_lock, flags); if (timer_pending(timer)) goto bug; internal_add_timer(timer); spin_unlock_irqrestore(&timerlist_lock, flags); return; bug: spin_unlock_irqrestore(&timerlist_lock, flags); printk("bug: kernel timer added twice at %p.\n", __builtin_return_address(0)); }函数internal_add_timer()用于将一个不处于任何定时器向量中的定时器插入到它应该所处的定时器向量中去(根据定时器的expires值来决定)。如下所示(kernel/timer.c): static inline void internal_add_timer(struct timer_list *timer) { /* * must be cli-ed when calling this */ unsigned long expires = timer->expires; unsigned long idx = expires - timer_jiffies; struct list_head * vec; if (idx < TVR_SIZE) { int i = expires & TVR_MASK; vec = tv1.vec + i; } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { int i = (expires >> TVR_BITS) & TVN_MASK; vec = tv2.vec + i; } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK; vec = tv3.vec + i; } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; vec = tv4.vec + i; } else if ((signed long) idx < 0) { /* can happen if you add a timer with expires == jiffies, * or you set a timer to go off in the past */ vec = tv1.vec + tv1.index; } else if (idx <= 0xffffffffUL) { int i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; vec = tv5.vec + i; } else { /* Can only get here on architectures with 64-bit jiffies */ INIT_LIST_HEAD(&timer->list); return; } /* * Timers are FIFO! */ list_add(&timer->list, vec->prev); }对该函数的注释如下:

(1)首先,计算定时器的expires值与timer_jiffies的插值(注意!这里应该使用动态定时器自己的时间基准),这个差值就表示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。局部变量idx保存这个差值。

(2)根据idx的值确定这个定时器应被插入到哪一个定时器向量中。其具体的确定方法我们在7.6.2节已经说过了,这里不再详述。最后,定时器向量的头部指针vec表示这个定时器应该所处的定时器向量链表头部。

(3)最后,调用list_add()函数将定时器插入到vec指针所指向的定时器队列的尾部。

7.6.3.5 修改一个定时器的expires值当一个定时器已经被插入到内核动态定时器链表中后,我们还可以修改该定时器的expires值。函数mod_timer()实现这一点。如下所示(kernel/timer.c): int mod_timer(struct timer_list *timer, unsigned long expires) { int ret; unsigned long flags; spin_lock_irqsave(&timerlist_lock, flags); timer->expires = expires; ret = detach_timer(timer); internal_add_timer(timer); spin_unlock_irqrestore(&timerlist_lock, flags); return ret; }该函数首先根据参数expires值更新定时器的expires成员。然后调用detach_timer()函数将该定时器从它原来所属的链表中删除。最后调用internal_add_timer()函数将该定时器根据它新的expires值重新插入到相应的链表中。函 数detach_timer()首先调用timer_pending()来判断指定的定时器是否已经处于某个链表中,如果定时器原来就不处于任何链表中, 则detach_timer()函数什么也不做,直接返回0值,表示失败。否则,就调用list_del()函数将定时器从它原来所处的链表中摘除。如下 所示(kernel/timer.c): static inline int detach_timer (struct timer_list *timer) { if (!timer_pending(timer)) return 0; list_del(&timer->list); return 1; }

7.6.3.6 删除一个定时器函数del_timer()用来将一个定时器从相应的内核定时器队列中删除。该函数实际上是对detach_timer()函数的高层封装。如下所示(kernel/timer.c): int del_timer(struct timer_list * timer) { int ret; unsigned long flags; spin_lock_irqsave(&timerlist_lock, flags); ret = detach_timer(timer); timer->list.next = timer->list.prev = NULL; spin_unlock_irqrestore(&timerlist_lock, flags); return ret; }

 7.6.3.7 定时器迁移操作由于一个定时器的interval值会随着时间的不断流逝(即jiffies值的不断增大)而不断变小,因此那些原本到期紧迫程度较低的定时器会随着jiffies值的不断增大而成为既把马上到期的定时器。比如定时器向量tv2.vec[0]中的定时器在经过256个时钟滴答后会成为未来256个时钟滴答内会到期的定时器。因此,定时器在内核动态定时器链表中的位置也应相应地随着改变。改变的规则是:当tv1.index重新变为0时(意味着tv1中的256个定时器向量都已被内核扫描一遍了,从而使tv1中的256个定时器向量变为空),则用tv2.vec[index]定时器向量中的定时器去填充tv1,同时使tv2.index加1(它以64为模)。当tv2.index重新变为0(意味着tv2中的64个定时器向量都已经被全部填充到tv1中去了,从而使得tv2变为空),则用tv3.vec[index]定时器向量中的定时器去填充tv2。如此一直类推下去,直到tv5。函数cascade_timers()完成这种定时器迁移操作,该函数只有一个timer_vec结构类型指针的参数tv。这个函数把把定时器向量tv->vec[tv->index]中的所有定时器重新填充到上一层定时器向量中去。如下所示(kernel/timer.c): static inline void cascade_timers(struct timer_vec *tv) { /* cascade all the timers from tv up one level */ struct list_head *head, *curr, *next; head = tv->vec + tv->index; curr = head->next; /* * We are removing _all_ timers from the list, so we don't have to * detach them individually, just clear the list afterwards. */ while (curr != head) { struct timer_list *tmp; tmp = list_entry(curr, struct timer_list, list); next = curr->next; list_del(curr); // not needed internal_add_timer(tmp); curr = next; } INIT_LIST_HEAD(head); tv->index = (tv->index + 1) & TVN_MASK; }对该函数的注释如下:

(1)首先,用指针head指向定时器头部向量头部的list_head结构。指针curr指向定时器向量中的第一个定时器。

(2)然后,用一个while{}循环来遍历定时器向量tv->vec[tv->index]。由于定时器向量是一个双向循环队列,因此循环的终止条件是curr=head。对于每一个被扫描的定时器,循环体都先调用list_del()函数把当前定时器从链表中摘除,然后调用internal_add_timer()函数重新确定该定时器应该被放到哪个定时器向量中去。

(3)当从while{}循环退出后,定时器向量tv->vec[tv->index]中所有的定时器都已被迁移到其它地方(到它们该呆的地方:-),因此它本身就成为一个空队列。这里我们显示地调用INIT_LIST_HEAD()宏来把定时器向量的表头结构初始化为空。

(4)最后,把tv->index值加1,当然它是以64为模。 以上文章内容均为网络转载或者本站会员原创,"Linux-cn.com不对文章内容做任何保证。


 7.6.4.8 

扫描并执行当前已经到期的定时器函数run_timer_list()完成这个功能。如前所述,该函数是被timer_bh()函数所调用的,因此内核定时器是在时钟中断的Bottom Half中被执行的。记住这一点非常重要。全局变量timer_jiffies表示了内核上一次执行run_timer_list()函数的时间,因此jiffies与timer_jiffies的差值就表示了自从上一次处理定时器以来,期间一共发生了多少次时钟中断,显然run_timer_list()函数必须为期间所发生的每一次时钟中断补上定时器服务。该函数的源码如下(kernel/timer.c): static inline void run_timer_list(void) { spin_lock_irq(&timerlist_lock); while ((long)(jiffies - timer_jiffies) >= 0) { struct list_head *head, *curr; if (!tv1.index) { int n = 1; do { cascade_timers(tvecs[n]); } while (tvecs[n]->index == 1 && ++n < NOOF_TVECS); } repeat: head = tv1.vec + tv1.index; curr = head->next; if (curr != head) { struct timer_list *timer; void (*fn)(unsigned long); unsigned long data; timer = list_entry(curr, struct timer_list, list); fn = timer->function; data= timer->data; detach_timer(timer); timer->list.next = timer->list.prev = NULL; timer_enter(timer); spin_unlock_irq(&timerlist_lock); fn(data); spin_lock_irq(&timerlist_lock); timer_exit(); goto repeat; } ++timer_jiffies; tv1.index = (tv1.index + 1) & TVR_MASK; } spin_unlock_irq(&timerlist_lock); }函数run_timer_list()的执行过程主要就是用一个大while{}循环来为时钟中断执行定时器服务,每一次循环服务一次时钟中断。因此一共要执行(jiffies-timer_jiffies+1)次循环。

循环体所执行的服务步骤如下:

(1)首先,判断tv1.index是否为0,如果为0则需要从tv2中补充定时器到tv1中来。但tv2也可能为空而需要从tv3中补充定时器,因此用一个do{}while循环来调用cascade_timer()函数来依次视需要从tv2中补充tv1,从tv3中补充tv2、…、从tv5中补充tv4。显然如果tvi.index=0(2≤i≤5),则对于tvi执行cascade_timers()函数后,tvi.index肯定为1。反过来讲,如果对tvi执行过cascade_timers()函数后tvi.index不等于1,那么可以肯定在未对tvi执行cascade_timers()函数之前,tvi.index值肯定不为0,因此这时tvi不需要从tv(i+1)中补充定时器,这时就可以终止do{}while循环。

(2)接下来,就要执行定时器向量tv1.vec[tv1.index]中的所有到期定时器。因此这里用一个goto repeat循环从头到尾依次扫描整个定时器对列。由于在执行定时器的关联函数时并不需要关CPU中断,所以在用detach_timer()函数把当前定时器从对列中摘除后,就可以调用spin_unlock_irq()函数进行解锁和开中断,然后在执行完当前定时器的关联函数后重新用spin_lock_irq()函数加锁和关中断。

(3)当执行完定时器向量tv1.vec[tv1.index]中的所有到期定时器后,tv1.vec[tv1.index]应该是个空队列。至此这一次定时器服务也就宣告结束。

(4)最后,把timer_jiffies值加1,把tv1.index值加1,当然它的模是256。然后,回到while循环开始下一次定时器服务。


要检查系统上的HZ的值是多少:

可执行命令:find kernel/ |xargs grep "CONFIG_HZ="        搜索得到,一般在kernel/.config文件里面


1.linux HZ

Linux核心几个重要跟时间有关的名词或变数,以下将介绍HZ、tick与jiffies。

HZ

Linux核心每隔固定周期会发出timer interrupt (IRQ 0),HZ是用来定义每一秒有几次timer interrupts。举例来说,HZ为1000,代表每秒有1000次timer interrupts。 HZ可在编译核心时设定,如下所示(以核心版本2.6.20-15为例):

 

adrian@adrian-desktop:~$ cd /usr/src/linux

adrian@adrian-desktop:/usr/src/linux$ make menuconfig

Processor type and features ---> Timer frequency (250 HZ) --->

其中HZ可设定100、250、300或1000。

小实验

观察/proc/interrupt的timer中断次数,并于一秒后再次观察其值。理论上,两者应该相差250左右。

adrian@adrian-desktop:~$ cat /proc/interrupts | grep timer && sleep 1 && cat /proc/interrupts | grep timer

0: 9309306 IO-APIC-edge timer

0: 9309562 IO-APIC-edge timer

上面四个栏位分别为中断号码、CPU中断次数、PIC与装置名称。

 

要检查系统上HZ的值是什么,就执行命令

cat kernel/.config | grep '^CONFIG_HZ='

2.Tick

Tick是HZ的倒数,意即timer interrupt每发生一次中断的时间。如HZ为250时,tick为4毫秒(millisecond)。

 

3.Jiffies

Jiffies为Linux核心变数(unsigned long),它被用来记录系统自开机以来,已经过了多少tick。每发生一次timer interrupt,Jiffies变数会被加一。值得注意的是,Jiffies于系统开机时,并非初始化成零,而是被设为-300*HZ (arch/i386/kernel/time.c),即代表系统于开机五分钟后,jiffies便会溢位。那溢位怎么办?事实上,Linux核心定义几个macro(timer_after、time_after_eq、time_before与time_before_eq),即便是溢位,也能借由这几个macro正确地取得jiffies的内容。

另外,80x86架构定义一个与jiffies相关的变数jiffies_64 ,此变数64位元,要等到此变数溢位可能要好几百万年。因此要等到溢位这刻发生应该很难吧。

 

3.1 jiffies及其溢出

全局变量jiffies取值为自操作系统启动以来的时钟滴答的数目,在头文件<linux/sched.h>中定义,数据类型为unsigned long volatile (32位无符号长整型)。

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。

3.4  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,然后才执行实际的比较。

8. 结论

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

内核时钟:

内核使用硬件提供的不同时钟来提供依赖于时间的服务,如busy-waiting(浪费CPU周期)和sleep-waiting(放弃CPU)

5.HZ and Jiffies

      jiffies记录了系统启动后的滴答数,常用的函数:time_before()、time_after()、time_after_eq()、time_before_eq()。因为jiffies随时钟滴答变化,不能用编译器优化它,应取volatile值。

 

      32位jiffies变量会在50天后溢出,太小,因此内核提供变量jiffies_64来hold 64位jiffies。该64位的低32位即为jiffies,在32位机上需要两天指令来赋值64位数据,不是原子的,因此内核提供函数get_jiffies_64()。

6.Long Delays

    busy-wait:timebefore(),使CPU忙等待;sleep-wait:shedule_timeout(截至时间);无论在内核空间还是用户空间,都没有比HZ更精确的控制了,因为时间片都是根据滴答更新的,而且即使定义了您的进程在超过指定时间后运行,调度器也可能根据优先级选择其他进程执行。

    sleep-wait():wait_event_timeout()用于在满足某个条件或超时后重新执行,msleep()睡眠指定的ms后重新进入就绪队列,这些长延迟仅适用于进程上下文,在中断上下文中不能睡眠也不能长时间busy-waiting。

内核提供了timer API来在一定时间后执行某个函数:

#include <linux/timer.h>

struct timer_list my_timer;

init_timer(&my_timer);            /* Also see setup_timer() */

my_timer.expire = jiffies + n*HZ; /* n is the timeout in number                                    of seconds */

my_timer.function = timer_func;   /* Function to execute

                                     after n seconds */

my_timer.data = func_parameter;   /* Parameter to be passed                                   to timer_func */

add_timer(&my_timer);                /*Start the timer*/

如果您想周期性执行上述代码,那么把它们加入timer_func()函数。您使用mod_timer()来改变my_timer的超时值,del_timer()来删掉my_timer,用timer_pending()查看是否my_timer处于挂起状态。

    用户空间函数clock_settime()和clock_gettime()用于获取内核时钟服务。用户应用程序使用setitimer()和getitimer()来控制alarm信号的传递当指定超时发生后。


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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值