本文主要介紹在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,分別用於保存時間日曆的具體信息,詳情以下:
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 格式以下:
bit[7]——UIP標誌(Update in Progress),爲1表示RTC正在更新日曆寄存器組中的值,此時日曆寄存器組是不可訪問的(此時訪問它們將獲得一個無心義的漸變值)。
bit
[6:4]——這三位是用來定義RTC的操做頻率。各類可能的值以下:
DV2 DV1 DV0
0 0 0 4.194304 MHZ
0 0 1 1.048576 MHZ
0 1 0 32.769 KHZ
1 1 0/1 任何
PC機一般設置成「010」。
bit
[3:0]——速率選擇位(Rate Selection bits),用於週期性或方波信號輸出。
RS3 RS2 RS1 RS0 週期性中斷 方波 週期性中斷 方波
0 0 0 0 None None None None
0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ
0 0 1 0 61.035μs 16.384 KHZ
0 0 1 1 122.070μs 8.192KHZ
0 1 0 0 244.141μs 4.096KHZ
0 1 0 1 488.281μs 2.048KHZ
0 1 1 0 976.562μs 1.024KHZ
0 1 1 1 1.953125ms 512HZ
1 0 0 0 3.90625ms 256HZ
1 0 0 1 7.8125ms 128HZ
1 0 1 0 15.625ms 64HZ
1 0 1 1 31.25ms 32HZ
1 1 0 0 62.5ms 16HZ
1 1 0 1 125ms 8HZ
1 1 1 0 250ms 4HZ
1 1 1 1 500ms 2HZ
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] – 設定各通道的工做模式。
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的值。利用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 1193180 kernel=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 100 kernel=2.4
#define HZ CONFIG_HZ kernel=2.6
(4)宏LATCH:定義要寫到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.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(),實現系統時間更新.