⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 linux_clock

📁 基于LINUX操作系统下的各种详细配置(如FTP
💻
📖 第 1 页 / 共 5 页
字号:
*/CMOS_WRITE(save_control, RTC_CONTROL);CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT);spin_unlock(&rtc_lock);return retval;}对该函数的注释如下:(1)首先对自旋锁rtc_lock进行加锁。定义在arch/i386/kernel/time.c文件中的全局自旋锁rtc_lock用来串行化所有CPU对RTC的操作。(2)接下来,在RTC控制寄存器中设置SET标志位,以便通知RTC软件程序随后马上将要更新它的时间与日期。为此先把RTC_CONTROL 寄存器的当前值读到变量save_control中,然后再把值(save_control | RTC_SET)回写到寄存器RTC_CONTROL中。(3)然后,通过RTC_FREQ_SELECT寄存器中bit[6:4]重启RTC芯片内部的除法器。为此,类似地先把 RTC_FREQ_SELECT寄存器的当前值读到变量save_freq_select中,然后再把值(save_freq_select | RTC_DIV_RESET2)回写到RTC_FREQ_SELECT寄存器中。(4)接着将RTC_MINUTES寄存器的当前值读到变量cmos_minutes中,并根据需要将它从BCD格式转化为二进制格式。(5)从nowtime参数中得到当前时间的秒数和分钟数。分别保存到real_seconds和real_minutes变量。注意,这里对于半小时区的情况要修正分钟数real_minutes的值。(6)然后,在real_minutes与RTC_MINUTES寄存器的原值cmos_minutes二者相差不超过30分钟的情况下,将 real_seconds和real_minutes所表示的时间值写到RTC的秒寄存器和分钟寄存器中。当然,在回写之前要记得把二进制转换为BCD格式。(7)最后,恢复RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原来的值。这二者的先后次序是:先恢复RTC_CONTROL寄存器,再恢复RTC_FREQ_SELECT寄存器。然后在解除自旋锁rtc_lock后就可以返回了。最后,需要说明的一点是,set_rtc_mmss()函数尽可能在靠近一秒时间间隔的中间位置(也即500ms处)左右被调用。此外, Linux内核对每一次成功的更新RTC时间都留下时间轨迹,它用一个系统全局变量last_rtc_update来表示内核最近一次成功地对RTC进行更新的时间(单位是秒数)。该变量定义在arch/i386/kernel/time.c文件中:/* last time the cmos clock got updated */static long last_rtc_update;每一次成功地调用set_rtc_mmss()函数后,内核都会马上将last_rtc_update更新为当前时间(具体请见7.4.3节)。7.3 Linux对时间的表示通常,操作系统可以使用三种方法来表示系统的当前时间与日期:①最简单的一种方法就是直接用一个64位的计数器来对时钟滴答进行计数。②第二种方法就是用一个32位计数器来对秒进行计数,同时还用一个32位的辅助计数器对时钟滴答计数,之子累积到一秒为止。因为232超过136年,因此这种方法直至22世纪都可以让系统工作得很好。③第三种方法也是按时钟滴答进行计数,但是是相对于系统启动以来的滴答次数,而不是相对于相对于某个确定的外部时刻;当读外部后备时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。UNIX类操作系统通常都采用第三种方法来维护系统的时间与日期。7.3.1 基本概念首先,有必要明确一些Linux内核时钟驱动中的基本概念。(1)时钟周期(clock cycle)的频率:8253/8254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率。Linux用宏 CLOCK_TICK_RATE来表示8254 PIT的输入时钟脉冲的频率(在PC机中这个值通常是1193180HZ),该宏定义在include/asm-i386/timex.h头文件中:#define CLOCK_TICK_RATE 1193180 /* Underlying HZ */(2)时钟滴答(clock tick):我们知道,当PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟中断,也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。(3)时钟滴答的频率(HZ):也即1秒时间内PIT所产生的时钟滴答次数。类似地,这个值也是由PIT通道0的计数器初值决定的(反过来说,确定了时钟滴答的频率值后也就可以确定8254 PIT通道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-01 00:00:00的相对秒数值。结构timeval是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示:struct timeval {time_t tv_sec; /* seconds */suseconds_t tv_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的更新并不是一项紧迫的任务,所以这一工作通常被延迟到时钟中断的底半部分(bottom half)中来进行。由于bottom half的执行时间带有不确定性,因此为了记住内核上一次更新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 {int tz_minuteswest; /* minutes west of Greenwich */int tz_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寄存器的宏操作x86 CPU的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 ()函数对计数器、时间中断的Bottom Half进行初始化。(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)设置8254 PIT的控制寄存器(端口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

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -