Linux的时间与时钟中断处理

来源:互联网 发布:数据中心网络电源系统 编辑:程序博客网 时间:2024/05/16 09:42

本文转自:http://blogold.chinaunix.net/u1/42058/showart_327378.html

本文主要介绍在Linux下的时间实现以及系统如何进行时钟中断处理。

一.       Linux的硬件时间

PC机中的时间有三种硬件时钟实现,这三种都是基于晶振产生的方波信号输入。这三种时钟为:(1实时时钟RTC ( Real Time Clock) 2可编程间隔器PIT(Programmable Interval Timer )3时间戳计数器TSC(Time Stamp Clock)

1.     实时时钟 RTC
  
用于长时间存放系统时间的设备,即时关机后也可依靠主板CMOS电池继续保持系统的计时,原理图如下:

Note: LinuxRTC的关系是,Linux启动时从RTC读取时间和日期的基准值,然后在Kernel运行期间便抛开RTC,以软件的形式维护系统的时间日期,并在适当时机由Kernel将时间写回RTC Register.

1.1 RTC Register 
   (1). 
时钟与日历Register
        
10个,地址:0x00-0x09,分别用于保存时间日历的具体信息,详情如下:

        00          Current Second for RTC
        01          Alarm Second

        02          Current Minute

        03          Alarm Minute

        04          Current Hour

        05          Alarm Hour

        06          Current Day of Week(1=Sunday)

        07          Current Date of Month

        08          Current Month

        09          Current Year

  (2).状态和控制Register

    共四个,地址:0x0a-0x0d,控制RTC芯片的工作方式,并表示当前状态。

l         状态RegisterA ,  0x0A 格式如下:

         bit7——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问的(此时访问它们将得到一个无意义的渐变值)。

bit64——这三位是用来定义RTC的操作频率。各种可能的值如下: 

DV2 DV1 DV0         
0             4.194304 MHZ 
            1.048576 MHZ 
            32.769   KHZ 
     0/1      
任何 
PC
机通常设置成“010”

bit30——速率选择位(Rate Selection bits),用于周期性或方波信号输出。 
RS3 RS2 RS1 RS0  
周期性中断   方波   周期性中断   方波
 
      0    None       None       None      None 
        30.517μs  32.768 KHZ 3.90625ms 256 HZ 
        61.035μs  16.384 KHZ 
        122.070μs  8.192KHZ 
         244.141μs 4.096KHZ 
  1        488.281μs 2.048KHZ 
         976.562μs 1.024KHZ 
         1.953125ms    512HZ 
         3.90625ms     256HZ 
      1    7.8125ms      128HZ 
         15.625ms      64HZ 
  0        31.25ms       32HZ 
         62.5ms        16HZ 
         125ms          8HZ 
1          250ms          4HZ 
      1    500ms          2HZ 
PC
BIOS对其默认的设置值是“0110”

l         状态Register B ,  0x0B 格式如下:

bit7——SET标志。为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。为0表示将允许更新过程继续。

bit6——PIE标志,周期性中断enable标志。

bit5——AIE标志,告警中断enable标志。

bit4——UIE标志,更新结束中断enable标志。

bit3——SQWE标志,方波信号enable标志。

bit2——DM标志,用来控制日历寄存器组的数据模式,0BCD1BINARYBIOS总是将它设置为0

bit1——2412标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。PCBIOS总是将它设置为1

bit0——DSE标志。BIOS总是将它设置为0

l         状态Register C0x0C 格式如下: 
bit
7——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求 发生。
 
bit
6——PF标志,周期性中断标志,为1表示发生周期性中断请求。
 
bit
5——AF标志,告警中断标志,为1表示发生告警中断请求。
 
bit
4——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。

l         状态Register D0x0D 格式如下: 
  bit
7——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC  已经掉电。
 
  bit
60——总是为0,未定义。

2.可编程间隔定时器 PIT

        每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号,作为系统定时器 system timer。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43 
Intel 8254 PIT
3个计时通道,每个通道都有其不同的用途:
 
1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0       系统 产生一次时钟中断。
 
2) 通道1通常用于控制DMACRAM的刷新。
 
3) 通道2被连接到PC机的扬声器,以产生方波信号。
 
    
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1.193181MHZ,也即一秒钟输入1193181clock-cycle。每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。计数器为16bit,因此所能表示的最大值是65536,一秒内发生的滴答数是:1193181/65536=18.206482.

        PITI/O端口
     0x40   
通道计数器  Read/Write
     0X41   
通道1计数器
  Read/Write
     0X42   
通道2计数器 
 Read/Write
     0X43   
控制字        Write Only

  NotePIT I/O端口是8位,而PIT相应计数器是16位,因此必须对PIT计数器进行两次读写。

   8254 PIT的控制寄存器(0X43)的格式如下

            bit[7:6] — 通道选择位:00 ,通道001,通道110,通道211read-back command,8254

            bit[5:4] – Read/Write/Latch锁定位,00,锁定当前计数器以便读取计数值;01,只读高字节;10,只读低字节;11,先高后低。

            bit[3:1] – 设定各通道的工作模式。

          000   mode0       当通道处于count out 时产生中断信号,可用于系统定时

          001   mode1       Hardware retriggerable one-shot

          010   mode2       Rate Generator。产生实时时钟中断,通道0通常工作在这个模式下

          011  mode3        方波信号发生器

          100  mode4        Software triggered strobe

          101  mode5        Hardware triggered strobe

3. 时间戳计数器 TSC

        Pentium开始,所有的Intel 80x86 CPU就都包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。 
    
汇编指令rdtsc可以用于读取TSC的值。利用CPUTSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ,那么TSC就将每2.5纳秒增加一次。

二.       Linux时钟中断处理程序

1.      几个概念

1时钟周期clock cycle)的频率:82538254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率Linux用宏 CLOCK_TICK_RATE来表示8254 PIT的输入时钟脉冲的频率(在PC机中这个值通常是1193180HZ),该宏定义在include/asm-i386/timex.h头文件中
#define CLOCK_TICK_RATE 1193180        kernel=2.4 &2.6

2时钟滴答clock tick):PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟中断,也即一次时钟滴答PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。

3时钟滴答的频率HZ):1秒时间内PIT所产生的时钟滴答次数。这个值也由PIT通道0的计数器初值决定的.Linux内核用宏HZ来表示时钟滴答的频率,而且在不同的平台上HZ有不同的定义值。对于ALPHAIA62平台HZ的值是1024,对于SPARCMIPSARMi386等平台HZ的值都是100。该宏在i386平台上的定义如下(include/asm-i386/param.h): 
#define HZ 100    kernel=2.4
#define HZ   CONFIG_HZ       kernel=2.6

4LATCH定义要写到PIT通道0的计数器中的值,它表示PIT将隔多少个时钟周期产生一次时钟中断。公式计算: 
LATCH
=(1秒之内的时钟周期个数)÷1秒之内的时钟中断次数)=(CLOCK_TICK_RATE÷HZ

定义在<include/linux/timex.h>
#define LATCH  ((CLOCK_TICK_RATE + HZ/2) / HZ)

(5)全局变量jiffies:用于记录系统自启动以来产生的滴答总数。启动时,kernel将该变量初始为0,每次时钟中断处理程序timer_interrupt()将该变量加1。因为一秒钟内增加的时钟中断次数等于Hz,所以jiffies一秒内增加的值也是Hz。由此可得系统运行时间是jiffies/Hz 秒。
jiffies
定义于<linux/jiffies.h>中:
extern unsigned long volatile jiffies;
Note:
kernel 2.4jiffies32位无符号数;kernel 2.6jiffies64位无符号数。

6全局变量xtime: 结构类型变量,用于表示当前时间距UNIX基准时间1970-01-01000000的相对秒数值。当系统启动时,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 registerlast_tsc_low

                     spin_lock (&i8253_lock);  //对自旋锁i8253_lock加锁,对8254PIT访问

                     outb_p (0x00, 0x43);     /* latch the count ASAP */

 

                     count = inb_p(0x40);    /* read the latched count */

                     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 it again in 600s */
……

  }

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 timerIRQ0上产生中断;kernel调用time_interrupt()time_interrupt()判断系统是否使用TSC,若使用则读取TSC register;然后读取PIT 通道0的计数值;调用do_time_interrupt(),实现系统时间更新.

原创粉丝点击