Linux定时器使用.docx
《Linux定时器使用.docx》由会员分享,可在线阅读,更多相关《Linux定时器使用.docx(13页珍藏版)》请在冰豆网上搜索。
Linux定时器使用
Linux定时器的使用
内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于和kernel/timer.c文件中。
被调度的函数肯定是异步执行的,它类似于一种“软件中断”,而且是处于非进程的上下文中,所以调度函数必须遵守以下规则:
1)没有current指针、不允许访问用户空间。
因为没有进程上下文,相关代码和被中断的进程没有任何联系。
2)不能执行休眠(或可能引起休眠的函数)和调度。
3)任何被访问的数据结构都应该针对并发访问进行保护,以防止竞争条件。
内核定时器的调度函数运行过一次后就不会再被运行了(相当于自动注销),但可以通过在被调度的函数中重新调度自己来周期运行。
在SMP系统中,调度函数总是在注册它的同一CPU上运行,以尽可能获得缓存的局域性。
内核定时器的数据结构
structtimer_list{
structlist_headentry;
unsignedlongexpires;
void(*function)(unsignedlong);
unsignedlongdata;
structtvec_base*base;
/*...*/
};
其中expires字段表示期望定时器执行的jiffies值,到达该jiffies值时,将调用function函数,并传递data作为参数。
当一个定时器被注册到内核之后,entry字段用来连接该定时器到一个内核链表中。
base字段是内核内部实现所用的。
需要注意的是expires的值是32位的,因为内核定时器并不适用于长的未来时间点。
初始化
在使用structtimer_list之前,需要初始化该数据结构,确保所有的字段都被正确地设置。
初始化有两种方法。
方法一:
DEFINE_TIMER(timer_name,function_name,expires_value,data);
该宏会定义一个名叫timer_name内核定时器,并初始化其function,expires,name和base字段。
方法二:
structtimer_listmytimer;
setup_timer(&mytimer,(*function)(unsignedlong),unsignedlongdata);
mytimer.expires=jiffies+5*HZ;
注意,无论用哪种方法初始化,其本质都只是给字段赋值,所以只要在运行add_timer()之前,expires,function和data字段都可以直接再修改。
关于上面这些宏和函数的定义,参见include/linux/timer.h。
注册
定时器要生效,还必须被连接到内核专门的链表中,这可以通过add_timer(structtimer_list*timer)来实现。
重新注册
要修改一个定时器的调度时间,可以通过调用mod_timer(structtimer_list*timer,unsignedlongexpires)。
mod_timer()会重新注册定时器到内核,而不管定时器函数是否被运行过。
注销
注销一个定时器,可以通过del_timer(structtimer_list*timer)或del_timer_sync(structtimer_list*timer)。
其中del_timer_sync是用在SMP系统上的(在非SMP系统上,它等于del_timer),当要被注销的定时器函数正在另一个cpu上运行时,del_timer_sync()会等待其运行完,所以这个函数会休眠。
另外还应避免它和被调度的函数争用同一个锁。
对于一个已经被运行过且没有重新注册自己的定时器而言,注销函数其实也没什么事可做。
inttimer_pending(conststructtimer_list*timer)
这个函数用来判断一个定时器是否被添加到了内核链表中以等待被调度运行。
注意,当一个定时器函数即将要被运行前,内核会把相应的定时器从内核链表中删除(相当于注销)
一个简单的例子
#include
#include
#include
structtimer_listmytimer;
staticvoidmyfunc(unsignedlongdata)
{
printk("%s\n",(char*)data);
mod_timer(&mytimer,jiffies+2*HZ);
}
staticint__initmytimer_init(void)
{
setup_timer(&mytimer,myfunc,(unsignedlong)"Hello,world!
");
mytimer.expires=jiffies+HZ;
add_timer(&mytimer);
return0;
}
staticvoid__exitmytimer_exit(void)
{
del_timer(&mytimer);
}
module_init(mytimer_init);
module_exit(mytimer_exit);
*******************************************************************************
----------------------------------------------------------------------------------------------------------------------
7.6.1 Linux内核对定时器的描述
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来描述一个内核定时器:
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):
Voidadd_timer(structtimer_list*timer){
unsignedlongflags;
spin_lock_irqsave(&timerlist_lock,flags);
if(timer_pending(timer))
gotobug;
internal_add_timer(timer);
spin_unlock_irqrestore(&timerlist_lock,flags);
return;
bug:
spin_unlock_irqrestore(&timerlist_lock,flags);
printk("bug:
kerneltimeraddedtwiceat%p.\n",__builtin_return_address(0));}
函数internal_add_timer()用于将一个不处于任何定时器向量中的定时器插入到它应该所处的定时器向量中去(根据定时器的expires值来决定)。
如下所示(kernel/timer.c):
staticinlinevoidinternal_add_timer(structtimer_list*timer){/**mustbecli-edwhencallingthis*/unsignedlongexpires=timer->expires;unsignedlongidx=expires-timer_jiffies;structlist_head*vec;