7.3 Linux对时间的表示
通常,操作系统可以使用三种方法来表示系统的当前时间与日期:①最简单的一种方法就是直接用一个64位的计数器来对时钟滴答进行计数。②第二种方法就是用一个32位计数器来对秒进行计数,同时还用一个32位的辅助计数器对时钟滴答计数,之子累积到一秒为止。因为232超过136年,因此这种方法直至22世纪都可以让系统工作得很好。③第三种方法也是按时钟滴答进行计数,但是是相对于系统启动以来的滴答次数,而不是相对于相对于某个确定的外部时刻;当读外部后备时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。
UNIX类操作系统通常都采用第三种方法来维护系统的时间与日期。
7.3.1 基本概念
首先,有必要明确一些Linux内核时钟驱动中的基本概念。
(1)时钟周期(clock cycle)的频率:8253/8254PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率。Linux用宏CLOCK_TICK_RATE来表示8254PIT的输入时钟脉冲的频率(在PC机中这个值通常是1193180HZ),该宏定义在include/asm-i386/timex.h头文件中:
#define CLOCK_TICK_RATE1193180 /* Underlying HZ */
(2)时钟滴答(clock tick):我们知道,当PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟中断,也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。
(3)时钟滴答的频率(HZ):也即1秒时间内PIT所产生的时钟滴答次数。类似地,这个值也是由PIT通道0的计数器初值决定的(反过来说,确定了时钟滴答的频率值后也就可以确定8254PIT通道0的计数器初值)。Linux内核用宏HZ来表示时钟滴答的频率,而且在不同的平台上HZ有不同的定义值。对于ALPHA和IA62平台HZ的值是1024,对于SPARC、MIPS、ARM和i386等平台HZ的值都是100。该宏在i386平台上的定义如下(include/asm-i386/param.h):
#ifndef HZ
#define HZ 100
#endif
根据HZ的值,我们也可以知道一次时钟滴答的具体时间间隔应该是(1000ms/HZ)=10ms。
(4)时钟滴答的时间间隔:Linux用全局变量tick来表示时钟滴答的时间间隔长度,该变量定义在kernel/timer.c文件中,如下:
long tick = (1000000 + HZ/2) / HZ;/* timer interrupt period */
tick变量的单位是微妙(μs),由于在不同平台上宏HZ的值会有所不同,因此方程式tick=1000000÷HZ的结果可能会是个小数,因此将其进行四舍五入成一个整数,所以Linux将tick定义成(1000000+HZ/2)/HZ,其中被除数表达式中的HZ/2的作用就是用来将tick值向上圆整成一个整型数。
另外,Linux还用宏TICK_SIZE来作为tick变量的引用别名(alias),其定义如下(arch/i386/kernel/time.c):
#define TICK_SIZE tick
(5)宏LATCH:Linux用宏LATCH来定义要写到PIT通道0的计数器中的值,它表示PIT将没隔多少个时钟周期产生一次时钟中断。显然LATCH应该由下列公式计算:
LATCH=(1秒之内的时钟周期个数)÷(1秒之内的时钟中断次数)=(CLOCK_TICK_RATE)÷(HZ)
类似地,上述公式的结果可能会是个小数,应该对其进行四舍五入。所以,Linux将LATCH定义为(include/linux/timex.h):
/* LATCH is used in the interval timer and ftape setup. */
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ)/* For divider */
类似地,被除数表达式中的HZ/2也是用来将LATCH向上圆整成一个整数。
7.3.2 表示系统当前时间的内核数据结构
作为一种UNIX类操作系统,Linux内核显然采用本节一开始所述的第三种方法来表示系统的当前时间。Linux内核在表示系统当前时间时用到了三个重要的数据结构:
①全局变量jiffies:这是一个32位的无符号整数,用来表示自内核上一次启动以来的时钟滴答次数。每发生一次时钟滴答,内核的时钟中断处理函数timer_interrupt()都要将该全局变量jiffies加1。该变量定义在kernel/timer.c源文件中,如下所示:
unsigned long volatile jiffies;
C语言限定符volatile表示jiffies是一个易该变的变量,因此编译器将使对该变量的访问从不通过CPU内部cache来进行。
②全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-0100:00:00的相对秒数值。结构timeval是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示:
struct timeval {
time_ttv_sec;/* seconds */
suseconds_ttv_usec;/* microseconds */
};
其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒之内的微秒值,且1000000>tv_usec>=0。
Linux内核通过timeval结构类型的全局变量xtime来维持当前时间,该变量定义在kernel/timer.c文件中,如下所示:
/* The current time */
volatile struct timeval xtime __attribute__ ((aligned (16)));
但是,全局变量xtime所维持的当前时间通常是供用户来检索和设置的,而其他内核模块通常很少使用它(其他内核模块用得最多的是jiffies),因此对xtime的更新并不是一项紧迫的任务,所以这一工作通常被延迟到时钟中断的底半部分(bottomhalf)中来进行。由于bottomhalf的执行时间带有不确定性,因此为了记住内核上一次更新xtime是什么时候,Linux内核定义了一个类似于jiffies的全局变量wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中:
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
③全局变量sys_tz:它是一个timezone结构类型的全局变量,表示系统当前的时区信息。结构类型timezone定义在include/linux/time.h头文件中,如下所示:
struct timezone {
inttz_minuteswest;/* minutes west of Greenwich */
inttz_dsttime;/* type of dst correction */
};
基于上述结构,Linux在kernel/time.c文件中定义了全局变量sys_tz表示系统当前所处的时区信息,如下所示:
struct timezone sys_tz;
7.3.3 Linux对TSC的编程实现
Linux用定义在arch/i386/kernel/time.c文件中的全局变量use_tsc来表示内核是否使用CPU的TSC寄存器,use_tsc=1表示使用TSC,use_tsc=0表示不使用TSC。该变量的值是在time_init()初始化函数中被初始化的(详见下一节)。该变量的定义如下:
static int use_tsc;
宏cpu_has_tsc可以确定当前系统的CPU是否配置有TSC寄存器。此外,宏CONFIG_X86_TSC也表示是否存在TSC寄存器。
7.3.3.1 读TSC寄存器的宏操作
x86CPU的rdtsc指令将TSC寄存器的高32位值读到EDX寄存器中、低32位读到EAX寄存器中。Linux根据不同的需要,在rdtsc指令的基础上封装几个高层宏操作,以读取TSC寄存器的值。它们均定义在include/asm-i386/msr.h头文件中,如下:
#define rdtsc(low,high)
__asm__ __volatile__("rdtsc" : "=a" (low), "=d" (high))
#define rdtscl(low)
__asm__ __volatile__ ("rdtsc" : "=a" (low) : : "edx")
#define rdtscll(val)
__asm__ __volatile__ ("rdtsc" : "=A" (val))
宏rdtsc()同时读取TSC的LSB与MSB,并分别保存到宏参数low和high中。宏rdtscl则只读取TSC寄存器的LSB,并保存到宏参数low中。宏rdtscll读取TSC的当前64位值,并将其保存到宏参数val这个64位变量中。
7.3.3.2 校准TSC
与可编程定时器PIT相比,用TSC寄存器可以获得更精确的时间度量。但是在可以使用TSC之前,它必须精确地确定1个TSC计数值到底代表多长的时间间隔,也即到底要过多长时间间隔TSC寄存器才会加1。Linux内核用全局变量fast_gettimeoffset_quotient来表示这个值,其定义如下(arch/i386/kernel/time.c):
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
根据上述定义的注释我们可以看出,这个变量的值是通过下述公式来计算的:
fast_gettimeoffset_quotient = (2^32) / (每微秒内的时钟周期个数)
定义在arch/i386/kernel/time.c文件中的函数calibrate_tsc()就是根据上述公式来计算fast_gettimeoffset_quotient的值的。显然这个计算过程必须在内核启动时完成,因此,函数calibrate_tsc()只被初始化函数time_init()所调用。
用TSC实现高精度的时间服务
在拥有TSC(TimeStamp Counter)的x86 CPU上,Linux内核可以实现微秒级的高精度定时服务,也即可以确定两次时钟中断之间的某个时刻的微秒级时间值。如下图所示:
图7-7 TSC时间关系
从上图中可以看出,要确定时刻x的微秒级时间值,就必须确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值(以微秒为单位)。为此,内核定义了以下两个变量:
(1)中断服务执行延迟delay_at_last_interrupt:由于从产生时钟中断的那个时刻到内核时钟中断服务函数timer_interrupt真正在CPU上执行的那个时刻之间是有一段延迟间隔的,因此,Linux内核用变量delay_at_last_interrupt来表示这一段时间延迟间隔,其定义如下(arch/i386/kernel/time.c):
/* Number of usecs that the last interrupt was delayed */
static int delay_at_last_interrupt;
关于delay_at_last_interrupt的计算步骤我们将在分析timer_interrupt()函数时讨论。
(2)全局变量last_tsc_low:它表示中断服务timer_interrupt真正在CPU上执行时刻的TSC寄存器值的低32位(LSB)。
显然,通过delay_at_last_interrupt、last_tsc_low和时刻x处的TSC寄存器值,我们就可以完全确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值。实现在arch/i386/kernel/time.c中的函数do_fast_gettimeoffset()就是这样计算时间间隔偏移的,当然它仅在CPU配置有TSC寄存器时才被使用,后面我们会详细分析这个函数。
7.4 时钟中断的驱动
如前所述,8253/8254 PIT的通道0通常被用来在IRQ0上产生周期性的时钟中断。对时钟中断的驱动是绝大数操作系统内核实现time-keeping的关键所在。不同的OS对时钟驱动的要求也不同,但是一般都包含下列要求内容:
1.维护系统的当前时间与日期。
2.防止进程运行时间超出其允许的时间。
3.对CPU的使用情况进行记帐统计。
4.处理用户进程发出的时间系统调用。
5.对系统某些部分提供监视定时器。
其中,第一项功能是所有OS都必须实现的基础功能,它是OS内核的运行基础。通常有三种方法可用来维护系统的时间与日期:(1)最简单的一种方法就是用一个64位的计数器来对时钟滴答进行计数。(2)第二种方法就是用一个32位计数器来对秒进行计数。用一个32位的辅助计数器来对时钟滴答计数直至累计一秒为止。因为232超过136年,因此这种方法直至22世纪都可以工作得很好。(3)第三种方法也是按滴答进行计数,但却是相对于系统启动以来的滴答次数,而不是相对于一个确定的外部时刻。当读后备时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。
UNIX类的OS通常都采用第三种方法来维护系统的时间与日期。
7.4.1 Linux对时钟中断的初始化
Linux对时钟中断的初始化是分为几个步骤来进行的:(1)首先,由init_IRQ()函数通过调用init_ISA_IRQ()函数对中断向量32~256所对应的中断向量描述符进行初始化设置。显然,这其中也就把IRQ0(也即中断向量32)的中断向量描述符初始化了。(2)然后,init_IRQ()函数设置中断向量32~256相对应的中断门。(3)init_IRQ()函数对PIT进行初始化编程;(4)sched_init()函数对计数器、时间中断的BottomHalf进行初始化。(5)最后,由time_init()函数对Linux内核的时钟中断机制进行初始化。这三个初始化函数都是由init/main.c文件中的start_kernel()函数调用的,如下:
asmlinkage void __init start_kernel()
{
…
trap_init();
init_IRQ();
sched_init();
time_init();
softirq_init();
…
}
(1)init_IRQ()函数对8254 PIT的初始化编程
函数init_IRQ()函数在完成中断门的初始化后,就对8254 PIT进行初始化编程设置,设置的步骤如下:(1)设置8254PIT的控制寄存器(端口0x43)的值为“01100100”,也即选择通道0、先读写LSB再读写MSB、工作模式2、二进制存储格式。(2)将宏LATCH的值写入通道0的计数器中(端口0x40),注意要先写LATCH的LSB,再写LATCH的高字节。其源码如下所示(arch/i386/kernel/i8259.c):
void __init init_IRQ(void)
{
……
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43);/* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40);/* LSB */
outb(LATCH >> 8 , 0x40);/* MSB */
……
}
(2)sched_init()对定时器机制和时钟中断的Bottom Half的初始化
函数sched_init()中与时间相关的初始化过程主要有两步:(1)调用init_timervecs()函数初始化内核定时器机制;(2)调用init_bh()函数将BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所对应的BH函数分别设置成timer_bh()、tqueue_bh()和immediate_bh()函数。如下所示(kernel/sched.c):
void __init sched_init(void)
{
……
init_timervecs();
init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
……
}
(3)time_init()函数对内核时钟中断机制的初始化
前面两个函数所进行的初始化步骤都是为时间中断机制做好准备而已。在执行完init_IRQ()函数和sched_init()函数后,CPU已经可以为IRQ0上的时钟中断进行服务了,因为IRQ0所对应的中断门已经被设置好指向中断服务函数IRQ0x20_interrupt()。但是由于此时中断向量0x20的中断向量描述符irq_desc[0]还是处于初始状态(其status成员的值为IRQ_DISABLED),并未挂接任何具体的中断服务描述符,因此这时CPU对IRQ0的中断服务并没有任何具体意义,而只是按照规定的流程空跑一趟。但是当CPU执行完time_init()函数后,情形就大不一样了。
函数time_init()主要做三件事:(1)从RTC中获取内核启动时的时间与日期;(2)在CPU有TSC的情况下校准TSC,以便为后面使用TSC做好准备;(3)在IRQ0的中断请求描述符中挂接具体的中断服务描述符。其源码如下所示(arch/i386/kernel/time.c):
void __init time_init(void)
{
extern int x86_udelay_tsc;
xtime.tv_sec = get_cmos_time();
xtime.tv_usec = 0;
/*
* If we have APM enabled or the CPU clock speed is variable
* (CPU stops clock on HLT or slows clock to save power)
* then the TSC timestamps may diverge by up to 1 jiffy from
* 'real time' but nothing will break.
* The most frequent case is that the CPU is "woken" from a halt
* state by the timer interrupt itself, so we get 0 error. In the
* rare cases where a driver would "wake" the CPU and request a
* timestamp, the maximum error is < 1 jiffy. But timestamps are
* still perfectly ordered.
* Note that the TSC counter will be reset if APM suspends
* to disk; this won't break the kernel, though, 'cuz we're
* smart. See arch/i386/kernel/apm.c.
*/
/*
*Firstly we have to do a CPU check for chips with
* a potentially buggy TSC. At this point we haven't run
*the ident/bugs checks so we must run this hook as it
*may turn off the TSC flag.
*
*NOTE: this doesnt yet handle SMP 486 machines where only
*some CPU's have a TSC. Thats never worked and nobody has
*moaned if you have the only one in the world - you fix it!
*/
dodgy_tsc();
if (cpu_has_tsc) {
unsigned long tsc_quotient = calibrate_tsc();
if (tsc_quotient) {
fast_gettimeoffset_quotient = tsc_quotient;
use_tsc = 1;
/*
*We could be more selective here I suspect
*and just enable this for the next intel chips ?
*/
x86_udelay_tsc = 1;
#ifndef do_gettimeoffset
do_gettimeoffset = do_fast_gettimeoffset;
#endif
do_get_fast_time = do_gettimeofday;
/* report CPU clock rate in Hz.
* The formula is (10^6 * 2^32) / (2^32 * 1 / (clocks/us)) =
* clock/second. Our precision is about 100 ppm.
*/
{unsigned long eax=0, edx=1000;
__asm__("divl %2"
:"=a" (cpu_khz), "=d" (edx)
:"r" (tsc_quotient),
"0" (eax), "1" (edx));
printk("Detected %lu.%03lu MHz processor.\n", cpu_khz / 1000, cpu_khz % 1000);
}
}
}
#ifdef CONFIG_VISWS
printk("Starting Cobalt Timer system clock\n");
/* Set the countdown value */
co_cpu_write(CO_CPU_TIMEVAL, CO_TIME_HZ/HZ);
/* Start the timer */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) | CO_CTRL_TIMERUN);
/* Enable (unmask) the timer interrupt */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) & ~CO_CTRL_TIMEMASK);
/* Wire cpu IDT entry to s/w handler (and Cobalt APIC to IDT) */
setup_irq(CO_IRQ_TIMER, &irq0);
#else
setup_irq(0, &irq0);
#endif
}
对该函数的注解如下:
(1)调用函数get_cmos_time()从RTC中得到系统启动时的时间与日期,它返回的是当前时间相对于1970-01-0100:00:00这个UNIX时间基准的秒数值。因此这个秒数值就被保存在系统全局变量xtime的tv_sec成员中。而xtime的另一个成员tv_usec则被初始化为0。
(2)通过dodgy_tsc()函数检测CPU是否存在时间戳记数器BUG(I know nothing about it:-)
(3)通过宏cpu_has_tsc来确定系统中CPU是否存在TSC计数器。如果存在TSC,那么内核就可以用TSC来获得更为精确的时间。为了能够用TSC来修正内核时间。这里必须作一些初始化工作:①调用calibrate_tsc()来确定TSC的每一次计数真正代表多长的时间间隔(单位为us),也即一个时钟周期的真正时间间隔长度。②将calibrate_tsc()函数所返回的值保存在全局变量fast_gettimeoffset_quotient中,该变量被用来快速地计算时间偏差;同时还将另一个全局变量use_tsc设置为1,表示内核可以使用TSC。这两个变量都定义在arch/i386/kernel/time.c文件中,如下:
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
……
static int use_tsc;
③接下来,将系统全局变量x86_udelay_tsc设置为1,表示可以通过TSC来实现微妙级的精确延时。该变量定义在arch/i386/lib/delay.c文件中。④将函数指针do_gettimeoffset强制性地指向函数do_fast_gettimeoffset()(与之对应的是do_slow_gettimeoffset()函数),从而使内核在计算时间偏差时可以用TSC这种快速的方法来进行。⑤将函数指针do_get_fast_time指向函数do_gettimeofday(),从而可以让其他内核模块通过do_gettimeofday()函数来获得更精准的当前时间。⑥计算并报告根据TSC所算得的CPU时钟频率。
(4)不考虑CONFIG_VISWS的情况,因此time_init()的最后一个步骤就是调用setup_irq()函数来为IRQ0挂接具体的中断服务描述符irq0。全局变量irq0是时钟中断请求的中断服务描述符,其定义如下(arch/i386/kernel/time.c):
static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
显然,函数timer_interrupt()将成为时钟中断的服务程序(ISR),而SA_INTERRUPT标志也指定了timer_interrupt()函数将是在CPU关中断的条件下执行的。结构irq0中的next指针被设置为NULL,因此IRQ0所对应的中断服务队列中只有irq0这唯一的一个元素,且IRQ0不允许中断共享。
7.4.2 时钟中断服务例程timer_interrupt()
中断服务描述符irq0一旦被钩挂到IRQ0的中断服务队列中去后,Linux内核就可以通过irq0->handler函数指针所指向的timer_interrupt()函数对时钟中断请求进行真正的服务,而不是向前面所说的那样只是让CPU“空跑”一趟。此时,Linux内核可以说是真正的“跳动”起来了。
在本节一开始所述的对时钟中断驱动的5项要求中,通常只有第一项(即timekeeping)是最为迫切的,因此必须在时钟中断服务例程中完成。而其余的几个要求可以稍缓,因此可以放在时钟中断的BottomHalf中去执行。这样,Linux内核就是timer_interrupt()函数的执行时间尽可能的短,因为它是在CPU关中断的条件下执行的。
函数timer_interrupt()的源码如下(arch/i386/kernel/time.c):
/*
* This is the same as the above, except we _also_ save the current
* Time Stamp Counter value at the time of the timer interrupt, so that
* we later on can estimate the time of day more exactly.
*/
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int count;
/*
* Here we are in the timer irq handler. We just have irqs locally
* disabled but we don't know if the timer_bh is running on the other
* CPU. We need to avoid to SMP race with it. NOTE: we don' t need
* the irq version of write_lock because as just said we have irq
* locally disabled. -arca
*/
write_lock(&xtime_lock);
if (use_tsc)
{
/*
* It is important that these two operations happen almost at
* the same time. We do the RDTSC stuff first, since it's
* faster. To avoid any inconsistencies, we need interrupts
* disabled locally.
*/
/*
* Interrupts are just disabled locally since the timer irq
* has the SA_INTERRUPT flag set. -arca
*/
/* read Pentium cycle counter */
rdtscl(last_tsc_low);
spin_lock(&i8253_lock);
outb_p(0x00, 0x43); /* latch the count ASAP */
count = inb_p(0x40); /* read the latched count */
count |= inb(0x40) << 8;
spin_unlock(&i8253_lock);
count = ((LATCH-1) - count) * TICK_SIZE;
delay_at_last_interrupt = (count + LATCH/2) / LATCH;
}
do_timer_interrupt(irq, NULL, regs);
write_unlock(&xtime_lock);
}
对该函数的注释如下:
(1)由于函数执行期间要访问全局时间变量xtime,因此一开就对自旋锁xtime_lock进行加锁。
(2)如果内核使用CPU的TSC寄存器(use_tsc变量非0),那么通过TSC寄存器来计算从时间中断的产生到timer_interrupt()函数真正在CPU上执行这之间的时间延迟:
l调用宏rdtscl()将64位的TSC寄存器值中的低32位(LSB)读到变量last_tsc_low中,以供do_fast_gettimeoffset()函数计算时间偏差之用。这一步的实质就是将CPUTSC寄存器的值更新到内核对TSC的缓存变量last_tsc_low中。
l 通过读8254PIT的通道0的计数器的当前值来计算时间延迟,为此:首先,对自旋锁i8253_lock进行加锁。自旋锁i8253_lock的作用就是用来串行化对8254PIT的读写访问。其次,向8254的控制寄存器(端口0x43)中写入值0x00,以便对通道0的计数器进行锁存。最后,通过端口0x40将通道0的计数器的当前值读到局部变量count中,并解锁i8253_lock。
l显然,从时间中断的产生到timer_interrupt()函数真正执行这段时间内,以一共流逝了((LATCH-1)-count)个时钟周期,因此这个延时长度可以用如下公式计算:
delay_at_last_interrupt=(((LATCH-1)-count)÷LATCH)﹡TICK_SIZE
显然,上述公式的结果是个小数,应对其进行四舍五入,为此,Linux用下述表达式来计算delay_at_last_interrupt变量的值:
(((LATCH-1)-count)*TICK_SIZE+LATCH/2)/LATCH
上述被除数表达式中的LATCH/2就是用来将结果向上圆整成整数的。
(3)在计算出时间延迟后,最后调用函数do_timer_interrupt()执行真正的时钟服务。
函数do_timer_interrupt()的源码如下(arch/i386/kernel/time.c):
/*
* timer_interrupt() needs to keep up the real-time clock,
* as well as call the "do_timer()" routine every clocktick
*/
static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
。。。。。。
do_timer(regs);
。。。。。。。
/*
* If we have an externally synchronized Linux clock, then update
* CMOS clock aclearcase/" target="_blank" >ccordingly every ~11 minutes. Set_rtc_mmss() has to be
* called as close as possible to 500 ms before the new second starts.
*/
if ((time_status & STA_UNSYNC) == 0 &&
xtime.tv_sec > last_rtc_update + 660 &&
xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 &&
xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) {
if (set_rtc_mmss(xtime.tv_sec) == 0)
last_rtc_update = xtime.tv_sec;
else
last_rtc_update = xtime.tv_sec - 600; /* do it again in 60 s */
}
……
}
上述代码中省略了许多与SMP相关的代码,因为我们不关心SMP。从上述代码我们可以看出,do_timer_interrupt()函数主要作两件事:
(1)调用do_timer()函数。
(2)判断是否需要更新CMOS时钟(即RTC)中的时间。Linux仅在下列三个条件同时成立时才更新CMOS时钟:①系统全局时间状态变量time_status中没有设置STA_UNSYNC标志,也即说明Linux有一个外部同步时钟。实际上全局时间状态变量time_status仅在一种情况下会被清除STA_SYNC标志,那就是执行adjtimex()系统调用时(这个syscall与NTP有关)。②自从上次CMOS时钟更新已经过去了11分钟。全局变量last_rtc_update保存着上次更新CMOS时钟的时间。③由于RTC存在UpdateCycle,因此最好在一秒时间间隔的中间位置500ms左右调用set_rtc_mmss()函数来更新CMOS时钟。因此Linux规定仅当全局变量xtime的微秒数tv_usec在500000±(tick/2)微秒范围范围之内时,才调用set_rtc_mmss()函数。如果上述条件均成立,那就调用set_rtc_mmss()将当前时间xtime.tv_sec更新回写到RTC中。
如果上面是的set_rtc_mmss()函数返回0值,则表明更新成功。于是就将“最近一次RTC更新时间”变量last_rtc_update更新为当前时间xtime.tv_sec。如果返回非0值,说明更新失败,于是就让last_rtc_update=xtime.tv_sec-600(相当于last_rtc_update+=60),以便在在60秒之后再次对RTC进行更新。
函数do_timer()实现在kernel/timer.c文件中,其源码如下:
void do_timer(struct pt_regs *regs)
{
(*(unsigned long *)&jiffies)++;
#ifndef CONFIG_SMP
/* SMP process accounting uses the local APIC timer */
update_process_times(user_mode(regs));
#endif
mark_bh(TIMER_BH);
if (TQ_ACTIVE(tq_timer))
mark_bh(TQUEUE_BH);
}
该函数的核心是完成三个任务:
(1)将表示自系统启动以来的时钟滴答计数变量jiffies加1。
(2)调用update_process_times()函数更新当前进程的时间统计信息。注意,该函数的参数原型是“intuser_tick”,如果本次时钟中断(即时钟滴答)发生时CPU正处于用户态下执行,则user_tick参数应该为1;否则如果本次时钟中断发生时CPU正处于核心态下执行时,则user_tick参数应改为0。所以这里我们以宏user_mode(regs)来作为update_process_times()函数的调用参数。该宏定义在include/asm-i386/ptrace.h头文件中,它根据regs指针所指向的核心堆栈寄存器结构来判断CPU进入中断服务之前是处于用户态下还是处于核心态下。如下所示:
#ifdef __KERNEL__
#define user_mode(regs) ((VM_MASK & (regs)->eflags) || (3 & (regs)->xcs))
……
#endif
(3)调用mark_bh()函数激活时钟中断的Bottom Half向量TIMER_BH和TQUEUE_BH(注意,TQUEUE_BH仅在任务队列tq_timer不为空的情况下才会被激活)。
至此,内核对时钟中断的服务流程宣告结束,下面我们详细分析一下update_process_times()函数的实现。
7.4.3 更新时间记帐信息——CPU分时的实现
函数update_process_times()被用来在发生时钟中断时更新当前进程以及内核中与时间相关的统计信息,并根据这些信息作出相应的动作,比如:重新进行调度,向当前进程发出信号等。该函数仅有一个参数user_tick,取值为1或0,其含义在前面已经叙述过。
该函数的源代码如下(kernel/timer.c):
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;
update_one_process(p, user_tick, system, cpu);
if (p->pid) {
if (--p->counter <= 0) {
p->counter = 0;
p->need_resched = 1;
}
if (p->nice > 0)
kstat.per_cpu_nice[cpu] += user_tick;
else
kstat.per_cpu_user[cpu] += user_tick;
kstat.per_cpu_system[cpu] += system;
} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)
kstat.per_cpu_system[cpu] += system;
}
(1)首先,用smp_processor_id()宏得到当前进程的CPU ID。
(2)然后,让局部变量system=user_tick^1,表示当发生时钟中断时CPU是否正处于核心态下。因此,如果user_tick=1,则system=0;如果user_tick=0,则system=1。
(3)调用update_one_process()函数来更新当前进程的task_struct结构中的所有与时间相关的统计信息以及成员变量。该函数还会视需要向当前进程发送相应的信号(signal)。
(4)如果当前进程的PID非0,则执行下列步骤来决定是否重新进行调度,并更新内核时间统计信息:
l将当前进程的可运行时间片长度(由task_struct结构中的counter成员表示,其单位是时钟滴答次数)减1。如果减到0值,则说明当前进程已经用完了系统分配给它的的运行时间片,因此必须重新进行调度。于是将当前进程的task_struct结构中的need_resched成员变量设置为1,表示需要重新执行调度。
l如果当前进程的task_struct结构中的nice成员值大于0,那么将内核全局统计信息变量kstat中的per_cpu_nice[cpu]值将上user_tick。否则就将user_tick值加到内核全局统计信息变量kstat中的per_cpu_user[cpu]成员上。
l将system变量值加到内核全局统计信息kstat.per_cpu_system[cpu]上。
(5)否则,就判断当前CPU在服务时钟中断前是否处于softirq软中断服务的执行中,或则正在服务一次低优先级别的硬件中断中。如果是这样的话,则将system变量的值加到内核全局统计信息kstat.per_cpu.system[cpu]上。
lupdate_one_process()函数
实现在kernel/timer.c文件中的update_one_process()函数用来在时钟中断发生时更新一个进程的task_struc结构中的时间统计信息。其源码如下(kernel/timer.c):
void update_one_process(struct task_struct *p, unsigned long user,
unsigned long system, int cpu)
{
p->per_cpu_utime[cpu] += user;
p->per_cpu_stime[cpu] += system;
do_process_times(p, user, system);
do_it_virt(p, user);
do_it_prof(p);
}
注释如下:
(1)由于在一个进程的整个生命期(Lifetime)中,它可能会在不同的CPU上执行,也即一个进程可能一开始在CPU1上执行,当它用完在CPU1上的运行时间片后,它可能又会被调度到CPU2上去执行。另外,当进程在某个CPU上执行时,它可能又会在用户态和内核态下分别各执行一段时间。所以为了统计这些事件信息,进程task_struct结构中的per_cpu_utime[NR_CPUS]数组就表示该进程在各CPU的用户台下执行的累计时间长度,per_cpu_stime[NR_CPUS]数组就表示该进程在各CPU的核心态下执行的累计时间长度;它们都以时钟滴答次数为单位。
所以,update_one_process()函数的第一个步骤就是更新进程在当前CPU上的用户态执行时间统计per_cpu_utime[cpu]和核心态执行时间统计per_cpu_stime[cpu]。
(2)调用do_process_times()函数更新当前进程的总时间统计信息。
(3)调用do_it_virt()函数为当前进程的ITIMER_VIRTUAL软件定时器更新时间间隔。
(4)调用do_it_prof()函数为当前进程的ITIMER_PROF软件定时器更新时间间隔。
ldo_process_times()函数
函数do_process_times()将更新指定进程的总时间统计信息。每个进程task_struct结构中都有一个成员times,它是一个tms结构类型(include/linux/times.h):
struct tms {
clock_t tms_utime; /* 本进程在用户台下的执行时间总和 */
clock_t tms_stime; /* 本进程在核心态下的执行时间总和 */
clock_t tms_cutime; /* 所有子进程在用户态下的执行时间总和 */
clock_t tms_cstime; /* 所有子进程在核心态下的执行时间总和 */
};
上述结构的所有成员都以时钟滴答次数为单位。
函数do_process_times()的源码如下(kernel/timer.c):
static inline void do_process_times(struct task_struct *p,
unsigned long user, unsigned long system)
{
unsigned long psecs;
psecs = (p->times.tms_utime += user);
psecs += (p->times.tms_stime += system);
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_cur) {
/* Send SIGXCPU every second.. */
if (!(psecs % HZ))
send_sig(SIGXCPU, p, 1);
/* and SIGKILL when we go over max.. */
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_max)
send_sig(SIGKILL, p, 1);
}
}
注释如下:
(1)根据参数user更新指定进程task_struct结构中的times.tms_utime值。根据参数system更新指定进程task_struct结构中的times.tms_stime值。
(2)将更新后的times.tms_utime值与times.tms_stime值的和保存到局部变量psecs中,因此psecs就表示了指定进程p到目前为止已经运行的总时间长度(以时钟滴答次数计)。如果这一总运行时间长超过进程P的资源限额,那就每隔1秒给进程发送一个信号SIGXCPU;如果运行时间长度超过了进程资源限额的最大值,那就发送一个SIGKILL信号杀死该进程。
ldo_it_virt()函数
每个进程都有一个用户态执行时间的itimer软件定时器。进程任务结构task_struct中的it_virt_value成员是这个软件定时器的时间计数器。当进程在用户态下执行时,每一次时钟滴答都使计数器it_virt_value减1,当减到0时内核向进程发送SIGVTALRM信号,并重置初值。初值保存在进程的task_struct结构的it_virt_incr成员中。
函数do_it_virt()的源码如下(kernel/timer.c):
static inline void do_it_virt(struct task_struct * p, unsigned long ticks)
{
unsigned long it_virt = p->it_virt_value;
if (it_virt) {
it_virt -= ticks;
if (!it_virt) {
it_virt = p->it_virt_incr;
send_sig(SIGVTALRM, p, 1);
}
p->it_virt_value = it_virt;
}
}
ldo_it_prof()函数
类似地,每个进程也都有一个itimer软件定时器ITIMER_PROF。进程task_struct中的it_prof_value成员就是这个定时器的时间计数器。不管进程是在用户态下还是在内核态下运行,每个时钟滴答都使it_prof_value减1。当减到0时内核就向进程发送SIGPROF信号,并重置初值。初值保存在进程task_struct结构中的it_prof_incr成员中。
函数do_it_prof()就是用来完成上述功能的,其源码如下(kernel/timer.c):
static inline void do_it_prof(struct task_struct *p)
{
unsigned long it_prof = p->it_prof_value;
if (it_prof) {
if (--it_prof == 0) {
it_prof = p->it_prof_incr;
send_sig(SIGPROF, p, 1);
}
p->it_prof_value = it_prof;
}
}