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 and
stuff 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_TSC
static 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内核的时钟机制!
阅读(1327) | 评论(0) | 转发(0) |