Linux定时器的使用.doc_第1页
Linux定时器的使用.doc_第2页
Linux定时器的使用.doc_第3页
Linux定时器的使用.doc_第4页
Linux定时器的使用.doc_第5页
已阅读5页,还剩8页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Linux定时器的使用内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于 和 kernel/timer.c 文件中。被调度的函数肯定是异步执行的,它类似于一种“软件中断”,而且是处于非进程的上下文中,所以调度函数必须遵守以下规则:1) 没有 current 指针、不允许访问用户空间。因为没有进程上下文,相关代码和被中断的进程没有任何联系。2) 不能执行休眠(或可能引起休眠的函数)和调度。3) 任何被访问的数据结构都应该针对并发访问进行保护,以防止竞争条件。内核定时器的调度函数运行过一次后就不会再被运行了(相当于自动注销),但可以通过在被调度的函数中重新调度自己来周期运行。在SMP系统中,调度函数总是在注册它的同一CPU上运行,以尽可能获得缓存的局域性。内核定时器的数据结构struct timer_list struct list_head entry; unsigned long expires; void (*function)(unsigned long); unsigned long data; struct tvec_base *base; /* . */;其中 expires 字段表示期望定时器执行的 jiffies 值,到达该 jiffies 值时,将调用 function 函数,并传递 data 作为参数。当一个定时器被注册到内核之后,entry 字段用来连接该定时器到一个内核链表中。base 字段是内核内部实现所用的。需要注意的是 expires 的值是32位的,因为内核定时器并不适用于长的未来时间点。初始化在使用 struct timer_list 之前,需要初始化该数据结构,确保所有的字段都被正确地设置。初始化有两种方法。方法一:DEFINE_TIMER(timer_name, function_name, expires_value, data);该宏会定义一个名叫 timer_name 内核定时器,并初始化其 function, expires, name 和 base 字段。方法二:struct timer_list mytimer;setup_timer(&mytimer, (*function)(unsigned long), unsigned long data);mytimer.expires = jiffies + 5*HZ;注意,无论用哪种方法初始化,其本质都只是给字段赋值,所以只要在运行 add_timer() 之前,expires, function 和 data 字段都可以直接再修改。关于上面这些宏和函数的定义,参见 include/linux/timer.h。注册定时器要生效,还必须被连接到内核专门的链表中,这可以通过 add_timer(struct timer_list *timer) 来实现。重新注册要修改一个定时器的调度时间,可以通过调用 mod_timer(struct timer_list *timer, unsigned long expires)。mod_timer() 会重新注册定时器到内核,而不管定时器函数是否被运行过。注销注销一个定时器,可以通过 del_timer(struct timer_list *timer) 或 del_timer_sync(struct timer_list *timer)。其中 del_timer_sync 是用在 SMP 系统上的(在非SMP系统上,它等于del_timer),当要被注销的定时器函数正在另一个 cpu 上运行时,del_timer_sync() 会等待其运行完,所以这个函数会休眠。另外还应避免它和被调度的函数争用同一个锁。对于一个已经被运行过且没有重新注册自己的定时器而言,注销函数其实也没什么事可做。int timer_pending(const struct timer_list *timer)这个函数用来判断一个定时器是否被添加到了内核链表中以等待被调度运行。注意,当一个定时器函数即将要被运行前,内核会把相应的定时器从内核链表中删除(相当于注销)一个简单的例子#include #include #include struct timer_list mytimer;static void myfunc(unsigned long data) printk(%sn, (char *)data); mod_timer(&mytimer, jiffies + 2*HZ);static int _init mytimer_init(void) setup_timer(&mytimer, myfunc, (unsigned long)Hello, world!); mytimer.expires = jiffies + HZ; add_timer(&mytimer); return 0;static void _exit mytimer_exit(void) del_timer(&mytimer);module_init(mytimer_init);module_exit(mytimer_exit); *-761Linux内核对定时器的描述Linux内核2.4版中去掉了老版本内核中的静态定时器机制,而只留下动态定时器。相应地在timer_bh()函数中也不再通过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。timer_create(2): 创建了一个定时器。timer_settime(2): 启动或者停止一个定时器。timer_gettime(2): 返回到下一次到期的剩余时间值和定时器定义的时间间隔。出现该接口的原因是,如果用户定义了一个 1ms 的定时器,可能当时系统负荷很重,导致该定时器实际山 10ms 后才超时,这种情况下,overrun=9ms 。timer_getoverrun(2): 返回上次定时器到期时超限值。timer_delete(2): 停止并删除一个定时器。上面最重要的接口是 timer_create(2),其中,clockid 表明了要使用的时钟类型,在 POSIX 中要求必须实现 CLOCK_REALTIME 类型的时钟。 evp 参数指明了在定时到期后,调用者被通知的方式。该结构体定义如下 :Linux在include/linux/timer.h头文件中定义了数据结构timer_list来描述一个内核定时器:structtimer_list structlist_headlist; unsignedlongexpires; unsignedlongdata; void(*function)(unsignedlong);各数据成员的含义如下:(1)双向链表元素list:用来将多个定时器连接成一条双向循环队列。(2)expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。当一个定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。(3)函数指针function:指向一个可执行函数。当定时器到期时,内核就执行function所指定的函数。而data域则被内核用作function函数的调用参数。内核函数init_timer()用来初始化一个定时器。实际上,这个初始化函数仅仅将结构中的list成员初始化为空。如下所示(include/linux/timer.h):staticinlinevoidinit_timer(structtimer_list*timer) timer-list.next=timer-list.prev=NULL;由于定时器通常被连接在一个双向循环队列中等待执行(此时我们说定时器处于pending状态)。因此函数time_pending()就可以用list成员是否为空来判断一个定时器是否处于pending状态。如下所示(include/linux/timer.h):staticinlineinttimer_pending(conststructtimer_list*timer) returntimer-list.next!=NULL;时间比较操作在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间关系比较操作宏。这里我们说时刻a在时刻b之后,就意味着时间值ab。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏(include/linux/timer.h):#definetime_after(a,b)(long)(b)-(long)(a)=0)#definetime_before_eq(a,b)time_after_eq(b,a)762动态内核定时器机制的原理Linux是怎样为其内核定时器机制提供动态扩展能力的呢?其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双向循环定时器队列(对列中的每一个元素都是一个timer_list结构):对列中的所有定时器都在同一个时刻到期,也即对列中的每一个timer_list结构都具有相同的expires值。显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中,这个时间差值的最大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指针,这显然是不现实的。另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!时间间隔是以滴答次数为计数单位的)。基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(intervalexpiresjiffies),则Linux采用了下列思想来实现其动态内核定时器机制:对于那些0interval255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256个定时器向量。而对于那些256interval0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。所谓“松散的定时器向量语义”就是指:各定时器的expires值可以互不相同的一个定时器队列。具体的组织方案可以分为两大部分:(1)对于内核最关心的、interval值在0,255之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义在kernel/timer.c文件中,如下述代码段所示:/*Eventtimercode*/#defineTVN_BITS6#defineTVR_BITS8#defineTVN_SIZE(1TVN_BITS)#defineTVR_SIZE(18)具有相同值的定时器都将被组织在同一个松散定时器向量中。因此,为组织所有满足条件0x100interval0x3fff的定时器,就需要2664个松散定时器向量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。基于数据结构timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如上述代码段所示。对于那些满足条件0x4000interval0xfffff的定时器,只要表达式(interval86)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000interval0xfffff的定时器,也需要2664个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。对于那些满足条件0x100000interval0x3ffffff的定时器,只要表达式(interval866)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x100000interval0x3ffffff的定时器,也需要2664个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4全局变量来表示这64个松散定时器向量。对于那些满足条件0x4000000interval0xffffffff的定时器,只要表达式(interval8666)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000interval0xffffffff的定时器,也需要2664个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5全局变量来表示这64个松散定时器向量。最后,为了引用方便,Linux定义了一个指针数组tvecs,来分别指向tv1、tv2、tv5结构变量。如上述代码所示。763内核动态定时器机制的实现在内核动态定时器机制的实现中,有三个操作时非常重要的:(1)将一个定时器插入到它应该所处的定时器向量中。(2)定时器的迁移,也即将一个定时器从它原来所处的定时器向量迁移到另一个定时器向量中。(3)扫描并执行当前已经到期的定时器。7631动态定时器机制的初始化函数init_timervecs()实现对动态定时器机制的初始化。该函数仅被sched_init()初始化例程所调用。动态定时器机制初始化过程的主要任务就是将tv1、tv2、tv5这5个结构变量中的定时器向量指针数组vec初始化为NULL。如下所示(kernel/timer.c):voidinit_timervecs(void)inti;for(i=0;iTVN_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;iexpires;unsignedlongidx=expires-timer_jiffies;structlist_head*vec;if(idxTVR_SIZE)inti=expires&TVR_MASK;vec=tv1.vec i;elseif(idx1TVR_BITS)&TVN_MASK;vec=tv2.vec i;elseif(idx1(TVR_BITS TVN_BITS)&TVN_MASK;vec=tv3.vec i;elseif(idx1(TVR_BITS 2*TVN_BITS)&TVN_MASK;vec=tv4.vec i;elseif(signedlong)idx0)/*canhappenifyouaddatimerwithexpires=jiffies,*oryousetatimertogooffinthepast*/vec=tv1.vec tv1.index;elseif(idx(TVR_BITS 3*TVN_BITS)&TVN_MASK;vec=tv5.vec i;else/*Canonlygethereonarchitectureswith64-bitjiffies*/INIT_LIST_HEAD(&timer-list);return;/*TimersareFIFO!*/list_add(&timer-list,vec-prev); 对该函数的注释如下:(1)首先,计算定时器的expires值与timer_jiffies的插值(注意!这里应该使用动态定时器自己的时间基准),这个差值就表示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。局部变量idx保存这个差值。(2)根据idx的值确定这个定时器应被插入到哪一个定时器向量中。其具体的确定方法我们在7.6.2节已经说过了,这里不再详述。最后,定时器向量的头部指针vec表示这个定时器应该所处的定时器向量链表头部。(3)最后,调用list_add()函数将定时器插入到vec指针所指向的定时器队列的尾部。7635修改一个定时器的expires值当一个定时器已经被插入到内核动态定时器链表中后,我们还可以修改该定时器的expires值。函数mod_timer()实现这一点。如下所示(kernel/timer.c): intmod_timer(structtimer_list*timer,unsignedlongexpires)intret;unsignedlongflags;spin_lock该函数首先根据参数expires值更新定时器的expires成员。然后调用detach_timer()函数将该定时器从它原来所属的链表中删除。最后调用internal_add_timer()函数将该定时器根据它新的expires值重新插入到相应的链表中。函数detach_timer()首先调用timer_pending()来判断指定的定时器是否已经处于某个链表中,如果定时器原来就不处于任何链表中,则detach_timer()函数什么也不做,直接返回0值,表示失败。否则,就调用list_del()函数将定时器从它原来所处的链表中摘除。如下所示(kernel/timer.c): staticinlineintdetach_timer(structtimer_list*timer)if(!timer_pending(timer)return0;list_del(&timer-list);return1; 7636删除一个定时器函数del_timer()用来将一个定时器从相应的内核定时器队列中删除。该函数实际上是对detach_timer()函数的高层封装。如下所示(kernel/timer.c): intdel_timer(structtimer_list*timer)intret;unsignedlongflags;spin_lock_irqsave(&timerlist_lock,flags);ret=detach_timer(timer);timer-list.next=timer-list.prev=NULL;spin_unlock_irqrestore(&timerlist_lock,flags);returnret; 软件开发网 7637定时器迁移操作由于一个定时器的interval值会随着时间的不断流逝(即jiffies值的不断增大)而不断变小,因此那些原本到期紧迫程度较低的定时器会随着jiffies值的不断增大而成为既将马上到期的定时器。比如定时器向量tv2.vec0中的定时器在经过256个时钟滴答后会成为未来256个时钟滴答内会到期的定时器。因此,定时器在内核动态定时器链表中的位置也应相应地随着改变。改变的规则是:当tv1.index重新变为0时(意味着tv1中的256个定时器向量都已被内核扫描一遍了,从而使tv1中的256个定时器向量变为空),则用tv2.vecindex定时器向量中的定时器去填充tv1,同时使tv2.index加1(它以64为模)。当tv2.index重新变为0(意味着tv2中的64个定时器向量都已经被全部填充到tv1中去了,从而使得tv2变为空),则用tv3.vecindex定时器向量中的定时器去填充tv2。如此一直类推下去,直到tv5。函数cascade_timers()完成这种定时器迁移操作,该函数只有一个timer_vec结构类型指针的参数tv。这个函数将把定时器向量tv-vectv-index中的所有定时器重新填充到上一层定时器向量中去。如下所示(kernel/timer.c): staticinlinevoidcascade_timers(structtimer_vec*tv)/*cascadeallthetimersfromtvuponelevel*/structlist_head*head,*curr,*next;head=tv-vec tv-index;curr=head-next;/*Weareremoving_all_timersfromthelist,sowedonthaveto*detachthemindividually,justclearthelistafterwards.*/while(curr!=head)structtimer_list*tmp;tmp=list_entry(curr,structtimer_list,list);next=curr-next;list_del(curr);/notneededinternal_add_timer(tmp);curr=next;INIT_LIST_HEAD(head);tv-index=(tv-index 1)&TVN_MASK; 对该函数的注释如下:(1)首先,用指针head指向定时器头部向量头部的list_head结构。指针curr指向定时器向量中的第一个定时器。(2)然后,用一个while循环来遍历定时器向量tv-vectv-index。由于定时器向量是一个双向循环队列,因此循环的终止条件是curr=head。对于每一个被扫描的定时器,循环体都先调用list_del()函数将当前定时器从链表中摘除,然后调用internal_add_timer()函数重新确定该定时器应该被放到哪个定时器向量中去。(3)当从while循环退出后,定时器向量tv-vectv-index中所有的定时器都已被迁移到其它地方(到它们该呆的地方:),因此它本身就成为一个空队列。这里我们显示地调用INIT_LIST_HEAD()宏来将定时器向量的表头结构初始化为空。(4)最后,将tv-index值加1,当然它是以64为模。 7648扫描并执行当前已经到期的定时器函数run_timer_list()完成这个功能。如前所述,该函数是被timer_bh()函数所调用的,因此内核定时器是在时钟中断的BottomHalf中被执行的。记住这一点非常重要。全局变量timer_jiffies表示了内核上一次执行run_timer_list()函数的时间,因此jiffies与timer_jiffies的差值就表示了自从上一次处理定时器以来,期间一共发生了多少次时钟中断,显然run_timer_list()函数必须为期间所发生的每一次时钟中断补上定时器服务。该函数的源码如下(kernel/timer.c): staticinlinevoidrun_timer_list(void)spin_lock_irq(&timerlist_lock);while(long)(jiffies-timer_jiffies)=0)structlist_head*head,*curr;if(!tv1.index)intn=1;docascade_timers(tvecsn);while(tvecsn-index=1& nnext;if(curr!=head)structtimer_list*timer;void(*fn)(unsignedlong);unsignedlongdata;timer=list_entry(curr,structtimer_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();gotorepeat; timer_jiffies;tv1.index=(tv1.index 1)&TVR_MASK;spin_unlock_irq(&timerlist_lock); 函数run_timer_list()的执行过程主要就是用一个大while循环来为时钟中断执行定时器服务,每一次循环服务一次时钟中断。因此一共要执行(jiffiestimer_jiffies1)次循环。循环体所执行的服务步骤如下:(1)首先,判断tv1.index是否为0,如果为0则需要从tv2中补充定时器到tv1中来。但tv2也可能为空而需要从tv3中补充定时器,因此用一个dowhile循环来调用cascade_timer()函数来依次视需要从tv2中补充tv1,从tv3中补充tv2、从tv5中补充tv4。显然如果tvi.index=0(2i5),则对于tvi执行cascade_timers()函数后,tvi.index肯定为1。反过来讲,如果对tvi执行过cascade_timers()函数后tvi.index不等于1,那么可以肯定在未对tvi执行cascade_timers()函数之前,tvi.index值肯定不为0,因此这时tvi不需要从tv(i 1)中补充定时器,这时就可以终止dowhile循环。(2)接下来,就要执行定时器向量tv1.vectv1.index中的所有到期定时器。因此这里用一个gotorepeat循环从头到尾依次扫描整个定时器对列。由于在执行定时器的关联函数时并不需要关CPU中断,所以在用detach_timer()函数将当前定时器从对列中摘除后,就可以调用spin_unlock_irq()函数进行解锁和开中断,然后在执行完当前定时器的关联函数后重新用spin_lock_irq()函数加锁和关中断。(2)然后,用一个while循环来遍历定时器向量tv-vectv-index。由于定时器向量是一个双向循环队列,因此循环的终止条件是curr=head。对于每一个被扫描的定时器,循环体都先调用list_del()函数将当前定时器从链表中摘除,然后调用internal_add_timer()函数重新确定该定时器应该被放到哪个定时器向量中去。(3)当从while循环退出后,定时器向量tv-vectv-index中所有的定时器都已被迁移到其它地方(到它们该呆的地方:),因此它本身就成为一个空队列。这里我们显示地调用INIT_LIST_HEAD()宏来将定时器向量的表头结构初始化为空。(4)最后,将tv-index值加1,当然它是以64为模。 7648扫描并执行当前已经到期的定时器函数run_timer_list()完成这个功能。如前所述,该函数是被timer_bh()函数所调用的,因此内核定时器是在时钟中断的BottomHalf中被执行的。记住这一点非常重要。全局变量timer_jiffies表示了内核上一次执行run_timer_list()函数的时间,因此jiffies与timer_jiffies的差值就表示了自从上一次处理定时器以来,期间一共发生了多少次时钟中断,显然run_timer_list()函数必须为期间所发生的每一次时钟中断补上定时器服务。该函数的源码如下(kernel/timer.

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论