Linux的时间与时钟中断处理

本文主要介绍在Linux下的时间实现以及系统如何进行时钟中断处理。 一.Linux的硬件时间 PC机中的时间有三种硬件时钟实现,这三种都是基于晶振产生的方波信号输入。这三种时钟为:(1)实时时钟RTC ( Real Time Clock) (2)可编程间隔器PIT(Programmable Interval Timer )(3)时间戳计数器TSC(Time Stamp Clock) 1.实时时钟 RTC
用于长时间存放系统时间的设备,即时关机后也可依靠主板CMOS电池继续保持系统的计时,原理图如下:
Note: Linux与RTC的关系是,当Linux启动时从RTC读取时间和日期的基准值,然后在Kernel运行期间便抛开RTC,以软件的形式维护系统的时间日期,并在适当时机由Kernel将时间写回RTC Register. 1.1 RTC Register
(1). 时钟与日历Register
共10个,地址:0x00-0x09,分别用于保存时间日历的具体信息,详情如下:
00Current Second for RTC
01Alarm Second 02Current Minute 03Alarm Minute 04Current Hour 05Alarm Hour 06Current Day of Week(1=Sunday) 07Current Date of Month 08Current Month 09Current Year (2).状态和控制Register 共四个,地址:0x0a-0x0d,控制RTC芯片的工作方式,并表示当前状态。 l状态RegisterA , 0x0A 格式如下: bit[7]——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问的(此时访问它们将得到一个无意义的渐变值)。 bit[6:4]——这三位是用来定义RTC的操作频率。各种可能的值如下:

DV2 DV1 DV0
0004.194304 MHZ
0011.048576 MHZ
01032.769KHZ
110/1任何
PC机通常设置成“010”。 bit[3:0]——速率选择位(Rate Selection bits),用于周期性或方波信号输出。
RS3 RS2 RS1 RS0周期性中断方波周期性中断方波
0000NoneNoneNoneNone
000130.517μs32.768 KHZ 3.90625ms 256 HZ
001061.035μs16.384 KHZ
0011122.070μs8.192KHZ
0100244.141μs 4.096KHZ
0101488.281μs 2.048KHZ
0110976.562μs 1.024KHZ
01111.953125ms512HZ
10003.90625ms256HZ
10017.8125ms128HZ
101015.625ms64HZ
101131.25ms32HZ
110062.5ms16HZ
1101125ms8HZ
1110250ms4HZ
1111500ms2HZ
PC机BIOS对其默认的设置值是“0110” l状态Register B , 0x0B 格式如下: bit[7]——SET标志。为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。为0表示将允许更新过程继续。 bit[6]——PIE标志,周期性中断enable标志。 bit[5]——AIE标志,告警中断enable标志。 bit[4]——UIE标志,更新结束中断enable标志。 bit[3]——SQWE标志,方波信号enable标志。 bit[2]——DM标志,用来控制日历寄存器组的数据模式,0=BCD,1=BINARY。BIOS总是将它设置为0。 bit[1]——24/12标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。PC机BIOS总是将它设置为1。 bit[0]——DSE标志。BIOS总是将它设置为0。 l状态Register C,0x0C 格式如下:
bit[7]——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求 发生。
bit[6]——PF标志,周期性中断标志,为1表示发生周期性中断请求。
bit[5]——AF标志,告警中断标志,为1表示发生告警中断请求。
bit[4]——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。 l状态Register D,0x0D 格式如下:
bit[7]——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC 已经掉电。
bit[6:0]——总是为0,未定义。 2.可编程间隔定时器 PIT 每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号,作为系统定时器 system timer。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统 产生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
每 个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1.193181MHZ,也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间 通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。计数 器为16bit,因此所能表示的最大值是65536,一秒内发生的滴答数是:1193181/65536=18.206482. PIT的I/O端口:
0x40通道0 计数器 Read/Write
0X41通道1计数器 Read/Write
0X42通道2计数器Read/Write
0X43控制字Write Only Note: 因PIT I/O端口是8位,而PIT相应计数器是16位,因此必须对PIT计数器进行两次读写。 8254 PIT的控制寄存器(0X43)的格式如下: bit[7:6] — 通道选择位:00 ,通道0;01,通道1;10,通道2;11,read-back command,仅8254。 bit[5:4] – Read/Write/Latch锁定位,00,锁定当前计数器以便读取计数值;01,只读高字节;10,只读低字节;11,先高后低。 bit[3:1] – 设定各通道的工作模式。 000mode0当通道处于count out 时产生中断信号,可用于系统定时 001mode1Hardware retriggerable one-shot 010mode2Rate Generator。产生实时时钟中断,通道0通常工作在这个模式下 011 mode3方波信号发生器 100 mode4Software triggered strobe 101 mode5Hardware triggered strobe 3. 时间戳计数器 TSC 从Pentium开始,所有的Intel 80x86 CPU就都包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。
汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ,那么TSC就将每2.5纳秒增加一次。 二.Linux时钟中断处理程序 1.几个概念 (1)时钟周期(clock cycle)的频率:8253/8254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率。Linux用宏 CLOCK_TICK_RATE来表示8254 PIT的输入时钟脉冲的频率(在PC机中这个值通常是1193180HZ),该宏定义在include/asm-i386/timex.h头文件中
#define CLOCK_TICK_RATE 1193180kernel=2.4 &2.6

(2)时钟滴答(clock tick):当PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟中断,也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。 (3)时钟滴答的频率(HZ):1秒时间内PIT所产生的时钟滴答次数。 这个值也由PIT通道0的计数器初值决定的.Linux内核用宏HZ来表示时钟滴答的频率,而且在不同的平台上HZ有不同的定义值。对于ALPHA和 IA62平台HZ的值是1024,对于SPARC、MIPS、ARM和i386等平台HZ的值都是100。该宏在i386平台上的定义如下 (include/asm-i386/param.h):
#define HZ 100kernel=2.4
#define HZCONFIG_HZkernel=2.6

(4)宏LATCH:定义要写到PIT通道0的计数器中的值,它表示PIT将隔多少个时钟周期产生一次时钟中断。公式计算:
LATCH=(1秒之内的时钟周期个数)÷(1秒之内的时钟中断次数)=(CLOCK_TICK_RATE)÷(HZ)
定义在
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) (5)全局变量jiffies:用于记录系统自启动以来产生的滴答总数。启动时,kernel将该变量初始为0,每次时钟中断处理程序timer_interrupt()将该变量加1。因为一秒钟内增加的时钟中断次数等于Hz,所以jiffies一秒内增加的值也是Hz。由此可得系统运行时间是jiffies/Hz 秒。
jiffies定义于中:
extern unsigned long volatile jiffies;
Note:在kernel 2.4,jiffies是32位无符号数;kernel 2.6,jiffies是64位无符号数。 (6)全局变量xtime: 结构类型变量,用于表示当前时间距UNIX基准时间1970-01-01 00:00:00的相对秒数值。当系统启动时,Kernel通过读取RTC Register中的数据来初始化系统时间(wall_time),该时间存放在xtime中。
void __init time_init (void) { ... ... xtime.tv_sec = get_cmos_time (); xtime.tv_usec = 0; ... ... }
Note:实时时钟RTC的最主要作用便是在系统启动时用来初始化xtime变量。 2.Linux的时钟中断处理程序 Linux下时钟中断处理由time_interrupt() 函数实现,主要完成以下任务: l获得xtime_lock锁,以便对访问的jiffies_64 (kernel2.6)和 xtime进行保护 l需要时应答或重新设置系统时钟。 l周期性的使用系统时间(wall_time)更新实时时钟RTC l调用体系结构无关的时钟例程:do_timer()。 do_timer()主要完成以下任务: l更新jiffies; l更新系统时间(wall_time),该时间存放在xtime变量中 l执行已经到期的动态定时器 l计算平均负载值
void do_timer(unsigned long ticks) { jiffies_64 += ticks;
update_process_times(user_mode(regs)); update_times (ticks); }
static inline void update_times(unsigned long ticks) { update_wall_time (); calc_load (ticks); } time_interrupt ():
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) {int count; write_lock (&xtime_lock); //获得xtime_lock锁 if(use_cyclone) mark_timeoffset_cyclone(); else if (use_tsc) { rdtscl(last_tsc_low); //读TSC register到last_tsc_low spin_lock (&i8253_lock); //对自旋锁i8253_lock加锁,对8254PIT访问 outb_p (0x00, 0x43); count = inb_p(0x40); count |= inb(0x40) << 8; if (count > LATCH) { printk (KERN_WARNING "i8253 count too high! resetting../n"); outb_p (0x34, 0x43); outb_p (LATCH & 0xff, 0x40); outb(LATCH >> 8, 0x40); count = LATCH - 1; } spin_unlock (&i8253_lock); if (count = = LATCH) { count- -; } count = ((LATCH-1) - count) * TICK_SIZE; delay_at_last_interrupt = (count + LATCH/2) / LATCH; } //end use_tsc do_timer_interrupt (irq, NULL, regs); write_unlock(&xtime_lock); }//end time_interrupt do_timer_interrupt():
static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) { ……
do_timer(regs);
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_timer_interrupt()主要完成:调用do_timer()和判断是否需要更新CMOS时钟。更新CMOS时钟的条件如下:三个须同时成立
1.系统全局时间状态变量time_status中没有设置STA_UNSYNC标志,即Linux没有设置外部同步时钟(如NTP) 2.自从上次CMOS时钟更新已经过去11分钟。全局变量last_rtc_update保存上次更新CMOS时钟的时间. 3.由于RTC存在Update Cycle,因此应在一秒钟间隔的中间500ms左右调用set_rtc_mmss()函数,将当前时间xtime.tv_sec写回RTC中。 Note. Linux kernel 中定义了一个类似jiffies的变量wall_jiffies,用于记录kernel上一次更新xtime时,jiffies的值。 Summary: Linux kernel在启动时,通过读取RTC里的时间日期初始化xtime,此后由kernel通过初始PIT来提供软时钟。 时钟中断处理过程可归纳为:系统时钟system timer在IRQ0上产生中断;kernel调用time_interrupt();time_interrupt()判断系统是否使用TSC,若使用 则读取TSC register; 然后读取PIT 通道0的计数值;调用do_time_interrupt(),实现系统时间更新.

    推荐阅读