所谓“间隔定时器(Interval Timer,简称itimer)就是指定时器采用“间隔”值(interval)来作为计时方式,当定时器启动后,间隔值interval将不断减小。当interval值减到0时,我们就说该间隔定时器到期。与上一节所说的内核动态定时器相比,二者最大的区别在于定时器的计时方式不同。内核定时器是通过它的到期时刻expires值来计时的,当全局变量jiffies值大于或等于内核动态定时器的expires值时,我们说内核内核定时器到期。而间隔定时器则实际上是通过一个不断减小的计数器来计时的。虽然这两种定时器并不相同,但却也是相互联系的。假如我们每个时钟节拍都使间隔定时器的间隔计数器减1,那么在这种情形下间隔定时器实际上就是内核动态定时器(下面我们会看到进程的真实间隔定时器就是这样通过内核定时器来实现的)。
间隔定时器主要被应用在用户进程上。每个Linux进程都有三个相互关联的间隔定时器。其各自的间隔计数器都定义在进程的task_struct结构中,如下所示(include/linux/sched.h):
struct task_struct{ …… unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; …… } |
(1)真实间隔定时器(ITIMER_REAL):这种间隔定时器在启动后,不管进程是否运行,每个时钟滴答都将其间隔计数器减1。当减到0值时,内核向进程发送SIGALRM信号。结构类型task_struct中的成员it_real_incr则表示真实间隔定时器的间隔计数器的初始值,而成员it_real_value则表示真实间隔定时器的间隔计数器的当前值。由于这种间隔定时器本质上与上一节的内核定时器时一样的,因此Linux实际上是通过real_timer这个内嵌在task_struct结构中的内核动态定时器来实现真实间隔定时器ITIMER_REAL的。
(2)虚拟间隔定时器ITIMER_VIRT:也称为进程的用户态间隔定时器。结构类型task_struct中成员it_virt_incr和it_virt_value分别表示虚拟间隔定时器的间隔计数器的初始值和当前值,二者均以时钟滴答次数位计数单位。当虚拟间隔定时器启动后,只有当进程在用户态下运行时,一次时钟滴答才能使间隔计数器当前值it_virt_value减1。当减到0值时,内核向进程发送SIGVTALRM信号(虚拟闹钟信号),并将it_virt_value重置为初值it_virt_incr。具体请见7.4.3节中的do_it_virt()函数的实现。
(3)PROF间隔定时器ITIMER_PROF:进程的task_struct结构中的it_prof_value和it_prof_incr成员分别表示PROF间隔定时器的间隔计数器的当前值和初始值(均以时钟滴答为单位)。当一个进程的PROF间隔定时器启动后,则只要该进程处于运行中,而不管是在用户态或核心态下执行,每个时钟滴答都使间隔计数器it_prof_value值减1。当减到0值时,内核向进程发送SIGPROF信号,并将it_prof_value重置为初值it_prof_incr。具体请见7.4.3节的do_it_prof()函数。
Linux在include/linux/time.h头文件中为上述三种进程间隔定时器定义了索引标识,如下所示:
#define ITIMER_REAL 0 #define ITIMER_VIRTUAL 1 #define ITIMER_PROF 2 |
虽然,在内核中间隔定时器的间隔计数器是以时钟滴答次数为单位,但是让用户以时钟滴答为单位来指定间隔定时器的间隔计数器的初值显然是不太方便的,因为用户习惯的时间单位是秒、毫秒或微秒等。所以Linux定义了数据结构itimerval来让用户以秒或微秒为单位指定间隔定时器的时间间隔值。其定义如下(include/linux/time.h):
struct itimerval { struct timeval it_interval; /* timer interval */ struct timeval it_value; /* current value */ }; |
其中,it_interval成员表示间隔计数器的初始值,而it_value成员表示间隔计数器的当前值。这两个成员都是timeval结构类型的变量,因此其精度可以达到微秒级。
timeval与jiffies之间的相互转换
由于间隔定时器的间隔计数器的内部表示方式与外部表现方式互不相同,因此有必要实现以微秒为单位的timeval结构和为时钟滴答次数单位的jiffies之间的相互转换。为此,Linux在kernel/itimer.c中实现了两个函数实现二者的互相转换——tvtojiffies()函数和jiffiestotv()函数。它们的源码如下:
static unsigned long tvtojiffies(struct timeval *value) { unsigned long sec = (unsigned) value->tv_sec; unsigned long usec = (unsigned) value->tv_usec; if (sec > (ULONG_MAX / HZ)) return ULONG_MAX; usec += 1000000 / HZ - 1; usec /= 1000000 / HZ; return HZ*sec+usec; } static void jiffiestotv(unsigned long jiffies, struct timeval *value) { value->tv_usec = (jiffies % HZ) * (1000000 / HZ); value->tv_sec = jiffies / HZ; } |
7.7.2 真实间隔定时器ITIMER_REAL的底层运行机制
间隔定时器ITIMER_VIRT和ITIMER_PROF的底层运行机制是分别通过函数do_it_virt()函数和do_it_prof()函数来实现的,这里就不再重述(可以参见7.4.3节)。
由于间隔定时器ITIMER_REAL本质上与内核动态定时器并无区别。因此内核实际上是通过内核动态定时器来实现进程的ITIMER_REAL间隔定时器的。为此,task_struct结构中专门设立一个timer_list结构类型的成员变量real_timer。动态定时器real_timer的函数指针function总是被task_struct结构的初始化宏INIT_TASK设置为指向函数it_real_fn()。如下所示(include/linux/sched.h):
#define INIT_TASK(tsk) \ …… real_timer: { function: it_real_fn \ } \ …… } |
而real_timer链表元素list和data成员总是被进程创建时分别初始化为空和进程task_struct结构的地址,如下所示(kernel/fork.c):
int do_fork(……) { …… p->it_real_value = p->it_virt_value = p->it_prof_value = 0; p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0; init_timer(&p->real_timer); p->real_timer.data = (unsigned long)p; …… } |
当用户通过setitimer()系统调用来设置进程的ITIMER_REAL间隔定时器时,it_real_incr被设置成非零值,于是该系统调用相应地设置好real_timer.expires值,然后进程的real_timer定时器就被加入到内核动态定时器链表中,这样该进程的ITIMER_REAL间隔定时器就被启动了。当real_timer定时器到期时,它的关联函数it_real_fn()将被执行。注意!所有进程的real_timer定时器的function函数指针都指向it_real_fn()这同一个函数,因此it_real_fn()函数必须通过其参数来识别是哪一个进程,为此它将unsigned long类型的参数p解释为进程task_struct结构的地址。该函数的源码如下
(kernel/itimer.c): void it_real_fn(unsigned long __data) { struct task_struct * p = (struct task_struct *) __data; unsigned long interval; send_sig(SIGALRM, p, 1); interval = p->it_real_incr; if (interval) { if (interval > (unsigned long) LONG_MAX) interval = LONG_MAX; p->real_timer.expires = jiffies + interval; add_timer(&p->real_timer); } } |
函数it_real_fn()的执行过程大致如下:
(1)首先将参数p通过强制类型转换解释为进程的task_struct结构类型的指针。
(2)向进程发送SIGALRM信号。
(3)在进程的it_real_incr非0的情况下继续启动real_timer定时器。首先,计算real_timer定时器的expires值为(jiffies+it_real_incr)。然后,调用add_timer()函数将real_timer加入到内核动态定时器链表中。
7.7.3 itimer定时器的系统调用
与itimer定时器相关的syscall有两个:getitimer()和setitimer()。其中,getitimer()用于查询调用进程的三个间隔定时器的信息,而setitimer()则用来设置调用进程的三个间隔定时器。这两个syscall都是现在kernel/itimer.c文件中。