Linux内核的时钟中断(4)

7.7 进程间隔定时器itimer7.8 时间系统调用的实现

7.7 进程间隔定时器itimer所谓“间隔定时器(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 27.7.1 数据结构itimerval虽然,在内核中间隔定时器的间隔计数器是以时钟滴答次数为单位,但是让用户以时钟滴答为单位来指定间隔定时器的间隔计数器的初值显然是不太方便 的,因为用户习惯的时间单位是秒、毫秒或微秒等。所以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结构类型的变量,因此其精度可以达到微秒级。l 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文件中。7.7.3.1 getitimer()系统调用的实现函数sys_getitimer()有两个参数:(1)which,指定查询调用进程的哪一个间隔定时器,其取值可以是ITIMER_REAL、 ITIMER_VIRT和ITIMER_PROF三者之一。(2)value指针,指向用户空间中的一个itimerval结构,用于接收查询结果。该函 数的源码如下:/* SMP: Only we modify our itimer values. */asmlinkage long sys_getitimer(int which, struct itimerval *value){int error = -EFAULT;struct itimerval get_buffer;if (value) {error = do_getitimer(which, &get_buffer);if (!error &&copy_to_user(value, &get_buffer, sizeof(get_buffer)))error = -EFAULT;}return error;}显然,sys_getitimer()函数主要通过do_getitimer()函数来查询当前进程的间隔定时器信息,并将查询结果保存在内核空 间的结构变量get_buffer中。然后,调用copy_to_usr()宏将get_buffer中结果拷贝到用户空间缓冲区中。函数do_getitimer()的源码如下(kernel/itimer.c):int do_getitimer(int which, struct itimerval *value){register unsigned long val, interval;switch (which) {case ITIMER_REAL:interval = current->it_real_incr;val = 0;/** FIXME! This needs to be atomic, in case the kernel timer happens!*/if (timer_pending(&current->real_timer)) {val = current->real_timer.expires – jiffies;/* look out for negative/zero itimer.. */if ((long) val <= 0)val = 1;}break;case ITIMER_VIRTUAL:val = current->it_virt_value;interval = current->it_virt_incr;break;case ITIMER_PROF:val = current->it_prof_value;interval = current->it_prof_incr;break;default:return(-EINVAL);}jiffiestotv(val, &value->it_value);jiffiestotv(interval, &value->it_interval);return 0;}查询的过程如下:(1)首先,用局部变量val和interval分别表示待查询间隔定时器的间隔计数器的当前值和初始值。(2)如果which=ITIMER_REAL,则查询当前进程的ITIMER_REAL间隔定时器。于是从current-> it_real_incr中得到ITIMER_REAL间隔定时器的间隔计数器的初始值,并将其保存到interval局部变量中。而对于间隔计数器的当 前值,由于ITITMER_REAL间隔定时器是通过real_timer这个内核动态定时器来实现的,因此不能通过current-> it_real_value来获得ITIMER_REAL间隔定时器的间隔计数器的当前值,而必须通过real_timer来得到这个值。为此先用 timer_pending()函数来判断current->real_timer是否已被起动。如果未启动,则说明ITIMER_REAL间隔定 时器也未启动,因此其间隔计数器的当前值肯定是0。因此将val变量简单地置0就可以了。如果已经启动,则间隔计数器的当前值应该等于 (timer_real.expires-jiffies)。(3)如果which=ITIMER_VIRT,则查询当前进程的ITIMER_VIRT间隔定时器。于是简单地将计数器初值it_virt_incr和当前值it_virt_value分别保存到局部变量interval和val中。(4)如果which=ITIMER_PROF,则查询当前进程的ITIMER_PROF间隔定时器。于是简单地将计数器初值it_prof_incr和当前值it_prof_value分别保存到局部变量interval和val中。(5)最后,通过转换函数jiffiestotv()将val和interval转换成timeval格式的时间值,并保存到value->it_value和value->it_interval中,作为查询结果返回。7.7.3.2 setitimer()系统调用的实现函数sys_setitimer()不仅设置调用进程的指定间隔定时器,而且还返回该间隔定时器的原有信息。它有三个参数:(1)which,含 义与sys_getitimer()中的参数相同。(2)输入参数value,指向用户空间中的一个itimerval结构,含有待设置的新值。(3)输 出参数ovalue,指向用户空间中的一个itimerval结构,用于接收间隔定时器的原有信息。该函数的源码如下(kernel/itimer.c):/* SMP: Again, only we play with our itimers, and signals are SMP safe* now so that is not an issue at all anymore.*/asmlinkage long sys_setitimer(int which, struct itimerval *value,struct itimerval *ovalue){struct itimerval set_buffer, get_buffer;int error;if (value) {if(copy_from_user(&set_buffer, value, sizeof(set_buffer)))return -EFAULT;} elsememset((char *) &set_buffer, 0, sizeof(set_buffer));error = do_setitimer(which, &set_buffer, ovalue ? &get_buffer : 0);if (error || !ovalue)return error;if (copy_to_user(ovalue, &get_buffer, sizeof(get_buffer)))return -EFAULT;return 0;}对该函数的注释如下:(1)在输入参数指针value非空的情况下,调用copy_from_user()宏将用户空间中的待设置信息拷贝到内核空间中的set_buffer结构变量中。如果value指针为空,则简单地将set_buffer结构变量全部置0。(2)调用do_setitimer()函数完成实际的设置操作。如果输出参数ovalue指针有效,则以内核变量get_buffer的地址作 为do_setitimer()函数的第三那个调用参数,这样当do_setitimer()函数返回时,get_buffer结构变量中就将含有当前进 程的指定间隔定时器的原来信息。Do_setitimer()函数返回0值表示成功,非0值表示失败。(3)在do_setitimer()函数返回非0值的情况下,或者ovalue指针为空的情况下(不需要输出间隔定时器的原有信息),函数就可以直接返回了。(4)如果ovalue指针非空,调用copy_to_user()宏将get_buffer()结构变量中值拷贝到ovalue所指向的用户空间中去,以便让用户得到指定间隔定时器的原有信息值。函数do_setitimer()的源码如下(kernel/itimer.c):int do_setitimer(int which, struct itimerval *value, struct itimerval *ovalue){register unsigned long i, j;int k;i = tvtojiffies(&value->it_interval);j = tvtojiffies(&value->it_value);if (ovalue && (k = do_getitimer(which, ovalue)) < 0)return k;switch (which) {case ITIMER_REAL:del_timer_sync(&current->real_timer);current->it_real_value = j;current->it_real_incr = i;if (!j)break;if (j > (unsigned long) LONG_MAX)j = LONG_MAX;i = j + jiffies;current->real_timer.expires = i;add_timer(&current->real_timer);break;case ITIMER_VIRTUAL:if (j)j++;current->it_virt_value = j;current->it_virt_incr = i;break;case ITIMER_PROF:if (j)j++;current->it_prof_value = j;current->it_prof_incr = i;break;default:return -EINVAL;}return 0;}对该函数的注释如下:(1)首先调用tvtojiffies()函数将timeval格式的初始值和当前值转换成以时钟滴答为单位的时间值。并分别保存在局部变量i和j中。(2)如果ovalue指针非空,则调用do_getitimer()函数查询指定间隔定时器的原来信息。如果do_getitimer()函数返回负值,说明出错。因此就要直接返回错误值。否则继续向下执行开始真正地设置指定的间隔定时器。(3)如果which=ITITMER_REAL,表示设置ITIMER_REAL间隔定时器。(a)调用del_timer_sync()函数 (该函数在单CPU系统中就是del_timer()函数)将当前进程的real_timer定时器从内核动态定时器链表中删除。(b)将 it_real_incr和it_real_value分别设置为局部变量i和j。(c)如果j=0,说明不必启动real_timer定时器,因此执行 break语句退出switch…case控制结构,而直接返回。(d)将real_timer的expires成员设置成(jiffies+当前值 j),然后调用add_timer()函数将当前进程的real_timer定时器加入到内核动态定时器链表中,从而启动该定时器。(4)如果which=ITIMER_VIRT,则简单地用局部变量i和j的值分别更新it_virt_incr和it_virt_value就可以了。(5)如果which=ITIMER_PROF,则简单地用局部变量i和j的值分别更新it_prof_incr和it_prof_value就可以了。(6)最后,返回0值表示成功。7.7.3.3 alarm系统调用系统调用alarm可以让调用进程在指定的秒数间隔后收到一个SIGALRM信号。它只有一个参数seconds,指定以秒数计的定时间隔。函数sys_alarm()的源码如下(kernel/timer.c):/** For backwards compatibility? This can be done in libc so Alpha* and all newer ports shouldn’t need it.*/asmlinkage unsigned long sys_alarm(unsigned int seconds){struct itimerval it_new, it_old;unsigned int oldalarm;it_new.it_interval.tv_sec = it_new.it_interval.tv_usec = 0;it_new.it_value.tv_sec = seconds;it_new.it_value.tv_usec = 0;do_setitimer(ITIMER_REAL, &it_new, &it_old);oldalarm = it_old.it_value.tv_sec;/* ehhh.. We can’t return 0 if we have an alarm pending.. *//* And we’d better return too much than too little anyway */if (it_old.it_value.tv_usec)oldalarm++;return oldalarm;}这个系统调用实际上就是启动进程的ITIMER_REAL间隔定时器。因此它完全可放到用户空间的C函数库(比如libc和glibc)中来实 现。但是为了保此内核的向后兼容性,2.4.0版的内核仍然将这个syscall放在内核空间中来实现。函数sys_alarm()的实现过程如下:(1)根据参数seconds的值构造一个itimerval结构变量it_new。注意!由于alarm启动的ITIMER_REAL间隔定时器是一次性而不是循环重复的,因此it_new变量中的it_interval成员一定要设置为0。(2)调用函数do_setitimer()函数以新构造的定时器it_new来启动当前进程的ITIMER_REAL定时器,同时将该间隔定时器的原定时间隔保存到局部变量it_old中。(3)返回值oldalarm表示以秒数计的ITIMER_REAL间隔定时器的原定时间隔值。因此先把 it_old.it_value.tv_sec赋给oldalarm,并且在it_old.it_value.tv_usec非0的情况下,将 oldalarm的值加1(也即不足1秒补足1秒)。7.8 时间系统调用的实现本节讲述与时间相关的syscall,这些系统调用主要用来供用户进程向内核检索当前时间与日期,因此他们是内核的时间服务接口。主要的时间系统 调用共有5个:time、stime和gettimeofday、settimeofday,以及与网络时间协议NTP相关的adjtimex系统调用。 这里我们不关心NTP,因此仅分析前4个时间系统调用。前4个时间系统调用可以分为两组:(1)time和stime是一组;(2) gettimeofday和settimeofday是一组。7.8.1 系统调用time和stime系统调用time()用于获取以秒数表示的系统当前时间(即内核全局时间变量xtime中的tv_sec成员的值)。它只有一个参数——整型指针 tloc,指向用户空间中的一个整数,用来接收返回的当前时间值。函数sys_time()的源码如下(kernel/time.c):asmlinkage long sys_time(int * tloc){int i;/* SMP: This is fairly trivial. We grab CURRENT_TIME andstuff it to user space. No side effects */i = CURRENT_TIME;if (tloc) {if (put_user(i,tloc))i = -EFAULT;}return i;}注释如下:(1)首先,函数调用CURRENT_TIME宏来得到以秒数表示的内核当前时间值,并将该值保存在局部变量i中。宏CURRENT_TIME定 义在include/linux/sched.h头文件中,它实际上就是内核全局时间变量xtime中的tv_sec成员。如下所示:#define CURRENT_TIME (xtime.tv_sec)(2)然后,在参数指针tloc非空的情况下将i的值通过put_user()宏传递到有tloc所指向的用户空间中去,以作为函数的输出结果。(3)最后,将局部变量I的值——也即也秒数表示的系统当前时间值作为返回值返回。系统调用stime()与系统调用time()刚好相反,它可以让用户设置系统的当前时间(以秒数为单位)。它同样也只有一个参数——整型指针tptr,指向用户空间中待设置的时间秒数值。函数sys_stime()的源码如下(kernel/time.c):asmlinkage long sys_stime(int * tptr){int value;if (!capable(CAP_SYS_TIME))return -EPERM;if (get_user(value, tptr))return -EFAULT;write_lock_irq(&xtime_lock);xtime.tv_sec = value;xtime.tv_usec = 0;time_adjust = 0; /* stop active adjtime() */time_status |= STA_UNSYNC;time_maxerror = NTP_PHASE_LIMIT;time_esterror = NTP_PHASE_LIMIT;write_unlock_irq(&xtime_lock);return 0;}注释如下:(1)首先检查调用进程的权限,显然,只有root用户才能有权限修改系统时间。(2)调用get_user()宏将tptr指针所指向的用户空间中的时间秒数值拷贝到内核空间中来,并保存到局部变量value中。(3)将局部变量value的值更新到全局时间变量xtime的tv_sec成员中,并将xtime的tv_usec成员清零。(4)在相应地重置其它状态变量后,函数就可以返回了(返回值0表示成功)。7.8.2 系统调用gettimeofday这个syscall用来供用户获取timeval格式的当前时间信息(精确度为微秒级),以及系统的当前时区信息(timezone)。结构类型 timeval的指针参数tv指向接受时间信息的用户空间缓冲区,参数tz是一个timezone结构类型的指针,指向接收时区信息的用户空间缓冲区。这 两个参数均为输出参数,返回值0表示成功,返回负值表示出错。函数sys_gettimeofday()的源码如下(kernel/time.c):asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz){if (tv) {struct timeval ktv;do_gettimeofday(&ktv);if (copy_to_user(tv, &ktv, sizeof(ktv)))return -EFAULT;}if (tz) {if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))return -EFAULT;}return 0;}显然,函数的实现主要分成两个大的方面:(1)如果tv指针有效,则说明用户要以timeval格式来检索系统当前时间。为此,先调用do_gettimeofday()函数来检索系统 当前时间并保存到局部变量ktv中。然后再调用copy_to_user()宏将保存在内核空间中的当前时间信息拷贝到由参数指针tv所指向的用户空间缓 冲区中。(2)如果tz指针有效,则说明用户要检索当前时区信息,因此调用copy_to_user()宏将全局变量sys_tz中的时区信息拷贝到参数指针tz所指向的用户空间缓冲区中。(3)最后,返回0表示成功。函数do_gettimeofday()的源码如下(arch/i386/kernel/time.c):/** This version of gettimeofday has microsecond resolution* and better than microsecond precision on fast x86 machines with TSC.*/void do_gettimeofday(struct timeval *tv){unsigned long flags;unsigned long usec, sec;read_lock_irqsave(&xtime_lock, flags);usec = do_gettimeoffset();{unsigned long lost = jiffies – wall_jiffies;if (lost)usec += lost * (1000000 / HZ);}sec = xtime.tv_sec;usec += xtime.tv_usec;read_unlock_irqrestore(&xtime_lock, flags);while (usec >= 1000000) {usec -= 1000000;sec++;}tv->tv_sec = sec;tv->tv_usec = usec;}该函数的完成实际的当前时间检索工作。由于gettimeofday()系统调用要求时间精度要达到微秒级,因此do_gettimeofday ()函数不能简单地返回xtime中的值即可,而必须精确地确定自从时钟驱动的Bottom Half上一次更新xtime的那个时刻(由wall_jiffies变量表示,参见7.3节)到do_gettimeofday()函数的当前执行时刻 之间的具体时间间隔长度,以便精确地修正xtime的值.如下图7-9所示:假定被do_gettimeofday()用来修正xtime的时间间隔为fixed_usec,而从wall_jiffies到 jiffies之间的时间间隔是lost_usec,而从jiffies到do_gettimeofday()函数的执行时刻的时间间隔是 offset_usec。则下列三个等式成立:fixed_usec=(lost_usec+offset_usec)lost_usec=(jiffies-wall_jiffies)*TICK_SIZE=(jiffies-wall_jiffies)*(1000000/HZ)由于全局变量last_tsc_low表示上一次时钟中断服务函数timer_interrupt()执行时刻的CPU TSC寄存器的值,因此我们可以用X86 CPU的TSC寄存器来计算offset_usec的值。也即:offset_usec=delay_at_last_interrupt+(current_tsc_low-last_tsc_low)*fast_gettimeoffset_quotient其中,delay_at_last_interrupt是从上一次发生时钟中断到timer_interrupt()服务函数真正执行时刻之间的 时间延迟间隔。每一次timer_interrupt()被执行时都会计算这一间隔,并利用TSC的当前值更新last_tsc_low变量(可以参见 7.4节)。假定current_tsc_low是do_gettimeofday()函数执行时刻TSC的当前值,全局变量 fast_gettimeoffset_quotient则表示TSC寄存器每增加1所代表的时间间隔值,它是由time_init()函数所计算的。根据上述原理分析,do_gettimeofday()函数的执行步骤如下:(1)调用函数do_gettimeoffset()计算从上一次时钟中断发生到执行do_gettimeofday()函数的当前时刻之间的时间间隔offset_usec。(2)通过wall_jiffies和jiffies计算lost_usec的值。(3)然后,令sec=xtime.tv_sec,usec=xtime.tv_usec+lost_usec+offset_usec。显然,sec表示系统当前时间在秒数量级上的值,而usec表示系统当前时间在微秒量级上的值。(4)用一个while{}循环来判断usec是否已经溢出而超过106us=1秒。如果溢出,则将usec减去106us并相应地将sec增加1,直到usec不溢出为止。(5)最后,用sec和usec分别更新参数指针所指向的timeval结构变量。至此,整个查询过程结束。函数do_gettimeoffset()根据CPU是否配置有TSC寄存器这一条件分别有不同的实现。其定义如下(arch/i386/kernel/time.c):#ifndef CONFIG_X86_TSCstatic unsigned long do_slow_gettimeoffset(void){……}static unsigned long (*do_gettimeoffset)(void) = do_slow_gettimeoffset;#else#define do_gettimeoffset() do_fast_gettimeoffset()#endif显然,在配置有TSC寄存器的i386平台上,do_gettimeoffset()函数实际上就是do_fast_gettimeoffset ()函数。它通过TSC寄存器来计算do_fast_gettimeoffset()函数被执行的时刻到上一次时钟中断发生时的时间间隔值。其源码如下 (arch/i386/kernel/time.c):static inline unsigned long do_fast_gettimeoffset(void){register unsigned long eax, edx;/* Read the Time Stamp Counter */rdtsc(eax,edx);/* .. relative to previous jiffy (32 bits is enough) */eax -= last_tsc_low; /* tsc_low delta *//** Time offset = (tsc_low delta) * fast_gettimeoffset_quotient* = (tsc_low delta) * (usecs_per_clock)* = (tsc_low delta) * (usecs_per_jiffy / clocks_per_jiffy)** Using a mull instead of a divl saves up to 31 clock cycles* in the critical path.*/__asm__("mull %2":"=a" (eax), "=d" (edx):"rm" (fast_gettimeoffset_quotient),"0" (eax));/* our adjusted time offset in microseconds */return delay_at_last_interrupt + edx;}对该函数的注释如下:(1)先调用rdtsc()函数读取当前时刻TSC寄存器的值,并将其高32位保存在edx局部变量中,低32位保存在局部变量eax中。(2)让局部变量eax=Δtsc_low=eax-last_tsc_low;也即计算当前时刻的TSC值与上一次时钟中断服务函数timer_interrupt()执行时的TSC值之间的差值。(3)显然,从上一次timer_interrupt()到当前时刻的时间间隔就是(Δtsc_low*fast_gettimeoffset_quotient)。因此用一条mul指令来计算这个乘法表达式的值。(4)返回值delay_at_last_interrupt+(Δtsc_low*fast_gettimeoffset_quotient)就是从上一次时钟中断发生时到当前时刻之间的时间偏移间隔值。7.8.3 系统调用settimeofday这个系统调用与gettimeofday()刚好相反,它供用户设置当前时间以及当前时间信息。它也有两个参数:(1)参数指针tv,指向含有待 设置时间信息的用户空间缓冲区;(2)参数指针tz,指向含有待设置时区信息的用户空间缓冲区。函数sys_settimeofday()的源码如下 (kernel/time.c):asmlinkage long sys_settimeofday(struct timeval *tv, struct timezone *tz){struct timeval new_tv;struct timezone new_tz;if (tv) {if (copy_from_user(&new_tv, tv, sizeof(*tv)))return -EFAULT;}if (tz) {if (copy_from_user(&new_tz, tz, sizeof(*tz)))return -EFAULT;}return do_sys_settimeofday(tv ? &new_tv : NULL, tz ? &new_tz : NULL);}函数首先调用copy_from_user()宏将保存在用户空间中的待设置时间信息和时区信息拷贝到内核空间中来,并保存到局部变量new_tv和new_tz中。然后,调用do_sys_settimeofday()函数完成实际的时间设置和时区设置操作。函数do_sys_settimeofday()的源码如下(kernel/time.c):int do_sys_settimeofday(struct timeval *tv, struct timezone *tz){static int firsttime = 1;if (!capable(CAP_SYS_TIME))return -EPERM;if (tz) {/* SMP safe, global irq locking makes it work. */sys_tz = *tz;if (firsttime) {firsttime = 0;if (!tv)warp_clock();}}if (tv){/* SMP safe, again the code in arch/foo/time.c should* globally block out interrupts when it runs.*/do_settimeofday(tv);}return 0;}该函数的执行过程如下:(1)首先,检查调用进程是否有相应的权限。如果没有,则返回错误值-EPERM。(2)如果执政tz有效,则用tz所指向的新时区信息更新全局变量sys_tz。并且如果是第一次设置时区信息,则在tv指针不为空的情况下调用 wrap_clock()函数来调整xtime中的秒数值。函数wrap_clock()的源码如下(kernel/time.c):inline static void warp_clock(void){write_lock_irq(&xtime_lock);xtime.tv_sec += sys_tz.tz_minuteswest * 60;write_unlock_irq(&xtime_lock);}(3)如果参数tv指针有效,则根据tv所指向的新时间信息调用do_settimeofday()函数来更新内核的当前时间xtime。(4)最后,返回0值表示成功。函数do_settimeofday()执行刚好与do_gettimeofday()相反的操作。这是因为全局变量xtime所表示的时间是与 wall_jiffies相对应的那一个时刻。因此,必须从参数指针tv所指向的新时间中减去时间间隔fixed_usec(其含义见7.8.2节)。函 数源码如下(arch/i386/kernel/time.c):void do_settimeofday(struct timeval *tv){write_lock_irq(&xtime_lock);/** This is revolting. We need to set "xtime" correctly. However, the* value in this location is the value at the most recent update of* wall time. Discover what correction gettimeofday() would have* made, and then undo it!*/tv->tv_usec -= do_gettimeoffset();tv->tv_usec -= (jiffies – wall_jiffies) * (1000000 / HZ);while (tv->tv_usec < 0) {tv->tv_usec += 1000000;tv->tv_sec–;}xtime = *tv;time_adjust = 0; /* stop active adjtime() */time_status |= STA_UNSYNC;time_maxerror = NTP_PHASE_LIMIT;time_esterror = NTP_PHASE_LIMIT;write_unlock_irq(&xtime_lock);}该函数的执行步骤如下:(1)调用do_gettimeoffset()函数计算上一次时钟中断发生时刻到当前时刻之间的时间间隔值。(2)通过wall_jiffies与jiffies计算二者之间的时间间隔lost_usec。(3)从tv->tv_usec中减去fixed_usec,即:tv->tv_usec-=(lost_usec+offset_usec)。(4)用一个while{}循环根据tv->tv_usec是否小于0来调整tv结构变量。如果tv->tv_usec小于0,则将 tv->tv_usec加上106us,并相应地将tv->tv_sec减1。直到tv->tv_usec不小于0为止。(5)用修正后的时间tv来更新内核全局时间变量xtime。(6)最后,重置其它时间状态变量。

至此,我们已经完全分析了整个Linux内核的时钟机制!

http://blog.chinaunix.net/uid-11449555-id-2873964.html

偶尔被惊鸿一瞥的美丽吸引;或者走进一条深沉深沉的巷道,

Linux内核的时钟中断(4)

相关文章:

你感兴趣的文章:

标签云: