Linux内核的时钟中断
生活随笔
收集整理的這篇文章主要介紹了
Linux内核的时钟中断
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
前言?
時間在一個操作系統內核中占據著重要的地位,它是驅動一個OS內核運行的“起博器”。一般說來,內核主要需要兩種類型的時間:?
1.?在內核運行期間持續記錄當前的時間與日期,以便內核對某些對象和事件作時間標記(timestamp,也稱為“時間戳”),或供用戶通過時間syscall進行檢索。?
2.?維持一個固定周期的定時器,以提醒內核或用戶一段時間已經過去了。?
PC機中的時間是有三種時鐘硬件提供的,而這些時鐘硬件又都基于固定頻率的晶體振蕩器來提供時鐘方波信號輸入。這三種時鐘硬件是:(1)實時時鐘(Real?Time?Clock,RTC);(2)可編程間隔定時器(Programmable?Interval?Timer,PIT);(3)時間戳計數器(Time?Stamp?Counter,TSC)。?
7.1?時鐘硬件?
7.1.1?實時時鐘RTC?
自從IBM?PC?AT起,所有的PC機就都包含了一個叫做實時時鐘(RTC)的時鐘芯片,以便在PC機斷電后仍然能夠繼續保持時間。顯然,RTC是通過主板上的電池來供電的,而不是通過PC機電源來供電的,因此當PC機關掉電源后,RTC仍然會繼續工作。通常,CMOS?RAM和RTC被集成到一塊芯片上,因此RTC也稱作“CMOS?Timer”。最常見的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容于MC146818,并有一定的擴展。本節內容主要基于MC146818這一標準的RTC芯片。具體內容可以參考MC146818的Datasheet。?
7.1.1.1?RTC寄存器?
MC146818?RTC芯片一共有64個寄存器。它們的芯片內部地址編號為0x00~0x3F(不是I/O端口地址),這些寄存器一共可以分為三組:?
(1)時鐘與日歷寄存器組:共有10個(0x00~0x09),表示時間、日歷的具體信息。在PC機中,這些寄存器中的值都是以BCD格式來存儲的(比如23dec=0x23BCD)。?
(2)狀態和控制寄存器組:共有4個(0x0A~0x0D),控制RTC芯片的工作方式,并表示當前的狀態。?
(3)CMOS配置數據:通用的CMOS?RAM,它們與時間無關,因此我們不關心它。?
時鐘與日歷寄存器組的詳細解釋如下:?
Address?Function?
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(01=Sunday)?
07?Current?date?of?month?
08?Current?month?
09?Current?year(final?two?digits,eg:93)?
狀態寄存器A(地址0x0A)的格式如下:?
其中:?
(1)bit[7]——UIP標志(Update?in?Progress),為1表示RTC正在更新日歷寄存器組中的值,此時日歷寄存器組是不可訪問的(此時訪問它們將得到一個無意義的漸變值)。?
(2)bit[6:4]——這三位是“除法器控制位”(divider-control?bits),用來定義RTC的操作頻率。各種可能的值如下:?
Divider?bits?Time-base?frequency?Divider?Reset?Operation?Mode?
DV2?DV1?DV0?
0?0?0?4.194304?MHZ?NO?YES?
0?0?1?1.048576?MHZ?NO?YES?
0?1?0?32.769?KHZ?NO?YES?
1?1?0/1?任何?YES?NO?
PC機通常將Divider?bits設置成“010”。?
(3)bit[3:0]——速率選擇位(Rate?Selection?bits),用于周期性或方波信號輸出。?
RS?bits?4.194304或1.048578?MHZ?32.768?KHZ?
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”。?
狀態寄存器B的格式如下所示:?
各位的含義如下:?
(1)bit[7]——SET標志。為1表示RTC的所有更新過程都將終止,用戶程序隨后馬上對日歷寄存器組中的值進行初始化設置。為0表示將允許更新過程繼續。?
(2)bit[6]——PIE標志,周期性中斷使能標志。?
(3)bit[5]——AIE標志,告警中斷使能標志。?
(4)bit[4]——UIE標志,更新結束中斷使能標志。?
(5)bit[3]——SQWE標志,方波信號使能標志。?
(6)bit[2]——DM標志,用來控制日歷寄存器組的數據模式,0=BCD,1=BINARY。BIOS總是將它設置為0。?
(7)bit[1]——24/12標志,用來控制hour寄存器,0表示12小時制,1表示24小時制。PC機BIOS總是將它設置為1。?
(8)bit[0]——DSE標志。BIOS總是將它設置為0。?
狀態寄存器C的格式如下:?
(1)bit[7]——IRQF標志,中斷請求標志,當該位為1時,說明寄存器B中斷請求發生。?
(2)bit[6]——PF標志,周期性中斷標志,為1表示發生周期性中斷請求。?
(3)bit[5]——AF標志,告警中斷標志,為1表示發生告警中斷請求。?
(4)bit[4]——UF標志,更新結束中斷標志,為1表示發生更新結束中斷請求。?
狀態寄存器D的格式如下:?
(1)bit[7]——VRT標志(Valid?RAM?and?Time),為1表示OK,為0表示RTC已經掉電。?
(2)bit[6:0]——總是為0,未定義。?
7.1.1.2?通過I/O端口訪問RTC?
在PC機中可以通過I/O端口0x70和0x71來讀寫RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是數據端口。?
讀RTC芯片寄存器的步驟是:?
mov?al,?addr?
out?70h,?al?;?Select?reg_addr?in?RTC?chip?
jmp?$+2?;?a?slight?delay?to?settle?thing?
in?al,?71h?;?
寫RTC寄存器的步驟如下:?
mov?al,?addr?
out?70h,?al?;?Select?reg_addr?in?RTC?chip?
jmp?$+2?;?a?slight?delay?to?settle?thing?
mov?al,?value?
out?71h,?al?
7.1.2?可編程間隔定時器PIT?
每個PC機中都有一個PIT,以通過IRQ0產生周期性的時鐘中斷信號。當前使用最普遍的是Intel?8254?PIT芯片,它的I/O端口地址是0x40~0x43。?
Intel?8254?PIT有3個計時通道,每個通道都有其不同的用途:?
(1)?通道0用來負責更新系統時鐘。每當一個時鐘滴答過去時,它就會通過IRQ0向系統產生一次時鐘中斷。?
(2)?通道1通常用于控制DMAC對RAM的刷新。?
(3)?通道2被連接到PC機的揚聲器,以產生方波信號。?
每個通道都有一個向下減小的計數器,8254?PIT的輸入時鐘信號的頻率是1193181HZ,也即一秒鐘輸入1193181個clock-cycle。每輸入一個clock-cycle其時間通道的計數器就向下減1,一直減到0值。因此對于通道0而言,當他的計數器減到0時,PIT就向系統產生一次時鐘中斷,表示一個時鐘滴答已經過去了。當各通道的計數器減到0時,我們就說該通道處于“Terminal?count”狀態。?
通道計數器的最大值是10000h,所對應的時鐘中斷頻率是1193181/(65536)=18.2HZ,也就是說,此時一秒鐘之內將產生18.2次時鐘中斷。?
7.1.2.1?PIT的I/O端口?
在i386平臺上,8254芯片的各寄存器的I/O端口地址如下:?
Port?Description?
40h?Channel?0?counter(read/write)?
41h?Channel?1?counter(read/write)?
42h?Channel?2?counter(read/write)?
43h?PIT?control?word(write?only)?
其中,由于通道0、1、2的計數器是一個16位寄存器,而相應的端口卻都是8位的,因此讀寫通道計數器必須進行進行兩次I/O端口讀寫操作,分別對應于計數器的高字節和低字節,至于是先讀寫高字節再讀寫低字節,還是先讀寫低字節再讀寫高字節,則由PIT的控制寄存器來決定。8254?PIT的控制寄存器的格式如下:?
(1)bit[7:6]——Select?Counter,選擇對那個計數器進行操作?!?0”表示選擇Counter?0,“01”表示選擇Counter?1,“10”表示選擇Counter?2,“11”表示Read-Back?Command(僅對于8254,對于8253無效)。?
(2)bit[5:4]——Read/Write/Latch格式位?!?0”表示鎖存(Latch)當前計數器的值;“01”只讀寫計數器的高字節(MSB);“10”只讀寫計數器的低字節(LSB);“11”表示先讀寫計數器的LSB,再讀寫MSB。?
(3)bit[3:1]——Mode?bits,控制各通道的工作模式。“000”對應Mode?0;“001”對應Mode?1;“010”對應Mode?2;“011”對應Mode?3;“100”對應Mode?4;“101”對應Mode?5。?
(4)bit[0]——控制計數器的存儲模式。0表示以二進制格式存儲,1表示計數器中的值以BCD格式存儲。?
7.1.2.2?PIT通道的工作模式?
PIT各通道可以工作在下列6種模式下:?
1.?Mode?0:當通道處于“Terminal?count”狀態時產生中斷信號。?
2.?Mode?1:Hardware?retriggerable?one-shot。?
3.?Mode?2:Rate?Generator。這種模式典型地被用來產生實時時鐘中斷。此時通道的信號輸出管腳OUT初始時被設置為高電平,并以此持續到計數器的值減到1。然后在接下來的這個clock-cycle期間,OUT管腳將變為低電平,直到計數器的值減到0。當計數器的值被自動地重新加載后,OUT管腳又變成高電平,然后重復上述過程。通道0通常工作在這個模式下。?
4.?Mode?3:方波信號發生器。?
5.?Mode?4:Software?triggered?strobe。?
6.?Mode?5:Hardware?triggered?strobe。?
7.1.2.3?鎖存計數器(Latch?Counter)?
當控制寄存器中的bit[5:4]設置成0時,將把當前通道的計數器值鎖存。此時通過I/O端口可以讀到一個穩定的計數器值,因為計數器表面上已經停止向下計數(PIT芯片內部并沒有停止向下計數)。NOTE!一旦發出了鎖存命令,就要馬上讀計數器的值。?
7.1.3?時間戳記數器TSC?
從Pentium開始,所有的Intel?80x86?CPU就都又包含一個64位的時間戳記數器(TSC)的寄存器。該寄存器實際上是一個不斷增加的計數器,它在CPU的每個時鐘信號到來時加1(也即每一個clock-cycle輸入CPU時,該計數器的值就加1)。?
匯編指令rdtsc可以用于讀取TSC的值。利用CPU的TSC,操作系統通??梢缘玫礁鼮榫珳实臅r間度量。假如clock-cycle的頻率是400MHZ,那么TSC就將每2.5納秒增加一次。
?dreamice 回復于:2008-11-06 17:56:02
.2?Linux內核對RTC的編程?
MC146818?RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上產生周期性的中斷,中斷的頻率在2HZ~8192HZ之間。與MC146818?RTC對應的設備驅動程序實現在include/linux/rtc.h和drivers/char/rtc.c文件中,對應的設備文件是/dev/rtc(major=10,minor=135,只讀字符設備)。因此用戶進程可以通過對她進行編程以使得當RTC到達某個特定的時間值時激活IRQ8線,從而將RTC當作一個鬧鐘來用。?
而Linux內核對RTC的唯一用途就是把RTC用作“離線”或“后臺”的時間與日期維護器。當Linux內核啟動時,它從RTC中讀取時間與日期的基準值。然后再運行期間內核就完全拋開RTC,從而以軟件的形式維護系統的當前時間與日期,并在需要時將時間回寫到RTC芯片中。?
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h頭文件中分別定義了mc146818?RTC芯片各寄存器的含義以及RTC芯片在i386平臺上的I/O端口操作。而通用的RTC接口則聲明在include/linux/rtc.h頭文件中。?
7.2.1?RTC芯片的I/O端口操作?
Linux在include/asm-i386/mc146818rtc.h頭文件中定義了RTC芯片的I/O端口操作。端口0x70被稱為“RTC端口0”,端口0x71被稱為“RTC端口1”,如下所示:?
#ifndef?RTC_PORT?
#define?RTC_PORT(x)?(0x70?+?(x))?
#define?RTC_ALWAYS_BCD?1?/*?RTC?operates?in?binary?mode?*/?
#endif?
顯然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。?
端口0x70被用作RTC芯片內部寄存器的地址索引端口,而端口0x71則被用作RTC芯片內部寄存器的數據端口。再讀寫一個RTC寄存器之前,必須先把該寄存器在RTC芯片內部的地址索引值寫到端口0x70中。根據這一點,讀寫一個RTC寄存器的宏定義CMOS_READ()和CMOS_WRITE()如下:?
#define?CMOS_READ(addr)?({?\?
outb_p((addr),RTC_PORT(0));?\?
inb_p(RTC_PORT(1));?\?
})?
#define?CMOS_WRITE(val,?addr)?({?\?
outb_p((addr),RTC_PORT(0));?\?
outb_p((val),RTC_PORT(1));?\?
})?
#define?RTC_IRQ?8?
在上述宏定義中,參數addr是RTC寄存器在芯片內部的地址值,取值范圍是0x00~0x3F,參數val是待寫入寄存器的值。宏RTC_IRQ是指RTC芯片所連接的中斷請求輸入線號,通常是8。?
7.2.2?對RTC寄存器的定義?
Linux在include/linux/mc146818rtc.h這個頭文件中定義了RTC各寄存器的含義。?
(1)寄存器內部地址索引的定義?
Linux內核僅使用RTC芯片的時間與日期寄存器組和控制寄存器組,地址為0x00~0x09之間的10個時間與日期寄存器的定義如下:?
#define?RTC_SECONDS?0?
#define?RTC_SECONDS_ALARM?1?
#define?RTC_MINUTES?2?
#define?RTC_MINUTES_ALARM?3?
#define?RTC_HOURS?4?
#define?RTC_HOURS_ALARM?5?
/*?RTC_*_alarm?is?always?true?if?2?MSBs?are?set?*/?
#?define?RTC_ALARM_DONT_CARE?0xC0?
#define?RTC_DAY_OF_WEEK?6?
#define?RTC_DAY_OF_MONTH?7?
#define?RTC_MONTH?8?
#define?RTC_YEAR?9?
四個控制寄存器的地址定義如下:?
#define?RTC_REG_A?10?
#define?RTC_REG_B?11?
#define?RTC_REG_C?12?
#define?RTC_REG_D?13?
(2)各控制寄存器的狀態位的詳細定義?
控制寄存器A(0x0A)主要用于選擇RTC芯片的工作頻率,因此也稱為RTC頻率選擇寄存器。因此Linux用一個宏別名RTC_FREQ_SELECT來表示控制寄存器A,如下:?
#define?RTC_FREQ_SELECT?RTC_REG_A?
RTC頻率寄存器中的位被分為三組:①bit[7]表示UIP標志;②bit[6:4]用于除法器的頻率選擇;③bit[3:0]用于速率選擇。它們的定義如下:?
#?define?RTC_UIP?0x80?
#?define?RTC_DIV_CTL?0x70?
/*?Periodic?intr.?/?Square?wave?rate?select.?0=none,?1=32.8kHz,...?15=2Hz?*/?
#?define?RTC_RATE_SELECT?0x0F?
正如7.1.1.1節所介紹的那樣,bit[6:4]有5中可能的取值,分別為除法器選擇不同的工作頻率或用于重置除法器,各種可能的取值如下定義所示:?
/*?divider?control:?refclock?values?4.194?/?1.049?MHz?/?32.768?kHz?*/?
#?define?RTC_REF_CLCK_4MHZ?0x00?
#?define?RTC_REF_CLCK_1MHZ?0x10?
#?define?RTC_REF_CLCK_32KHZ?0x20?
/*?2?values?for?divider?stage?reset,?others?for?"testing?purposes?only"?*/?
#?define?RTC_DIV_RESET1?0x60?
#?define?RTC_DIV_RESET2?0x70?
寄存器B中的各位用于使能/禁止RTC的各種特性,因此控制寄存器B(0x0B)也稱為“控制寄存器”,Linux用宏別名RTC_CONTROL來表示控制寄存器B,它與其中的各標志位的定義如下所示:?
#define?RTC_CONTROL?RTC_REG_B?
#?define?RTC_SET?0x80?/*?disable?updates?for?clock?setting?*/?
#?define?RTC_PIE?0x40?/*?periodic?interrupt?enable?*/?
#?define?RTC_AIE?0x20?/*?alarm?interrupt?enable?*/?
#?define?RTC_UIE?0x10?/*?update-finished?interrupt?enable?*/?
#?define?RTC_SQWE?0x08?/*?enable?square-wave?output?*/?
#?define?RTC_DM_BINARY?0x04?/*?all?time/date?values?are?BCD?if?clear?*/?
#?define?RTC_24H?0x02?/*?24?hour?mode?-?else?hours?bit?7?means?pm?*/?
#?define?RTC_DST_EN?0x01?/*?auto?switch?DST?-?works?f.?USA?only?*/?
寄存器C是RTC芯片的中斷請求狀態寄存器,Linux用宏別名RTC_INTR_FLAGS來表示寄存器C,它與其中的各標志位的定義如下所示:?
#define?RTC_INTR_FLAGS?RTC_REG_C?
/*?caution?-?cleared?by?read?*/?
#?define?RTC_IRQF?0x80?/*?any?of?the?following?3?is?active?*/?
#?define?RTC_PF?0x40?
#?define?RTC_AF?0x20?
#?define?RTC_UF?0x10?
寄存器D僅定義了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也稱為RTC的有效寄存器。Linux用宏別名RTC_VALID來表示寄存器D,如下:?
#define?RTC_VALID?RTC_REG_D?
#?define?RTC_VRT?0x80?/*?valid?RAM?and?time?*/?
(3)二進制格式與BCD格式的相互轉換?
由于時間與日期寄存器中的值可能以BCD格式存儲,也可能以二進制格式存儲,因此需要定義二進制格式與BCD格式之間的相互轉換宏,以方便編程。如下:?
#ifndef?BCD_TO_BIN?
#define?BCD_TO_BIN(val)?((val)=((val)&15)?+?((val)>>4)*10)?
#endif?
#ifndef?BIN_TO_BCD?
#define?BIN_TO_BCD(val)?((val)=(((val)/10)<<4)?+?(val)%10)?
#endif?
7.2.3?內核對RTC的操作?
如前所述,Linux內核與RTC進行互操作的時機只有兩個:(1)內核在啟動時從RTC中讀取啟動時的時間與日期;(2)內核在需要時將時間與日期回寫到RTC中。為此,Linux內核在arch/i386/kernel/time.c文件中實現了函數get_cmos_time()來進行對RTC的第一種操作。顯然,get_cmos_time()函數僅僅在內核啟動時被調用一次。而對于第二種操作,Linux則同樣在arch/i386/kernel/time.c文件中實現了函數set_rtc_mmss(),以支持向RTC中回寫當前時間與日期。下面我們將來分析這二個函數的實現。?
在分析get_cmos_time()函數之前,我們先來看看RTC芯片對其時間與日期寄存器組的更新原理。?
(1)Update?In?Progress?
當控制寄存器B中的SET標志位為0時,MC146818芯片每秒都會在芯片內部執行一個“更新周期”(Update?Cycle),其作用是增加秒寄存器的值,并檢查秒寄存器是否溢出。如果溢出,則增加分鐘寄存器的值,如此一致下去直到年寄存器。在“更新周期”期間,時間與日期寄存器組(0x00~0x09)是不可用的,此時如果讀取它們的值將得到未定義的值,因為MC146818在整個更新周期期間會把時間與日期寄存器組從CPU總線上脫離,從而防止軟件程序讀到一個漸變的數據。?
在MC146818的輸入時鐘頻率(也即晶體增蕩器的頻率)為4.194304MHZ或1.048576MHZ的情況下,“更新周期”需要花費248us,而對于輸入時鐘頻率為32.768KHZ的情況,“更新周期”需要花費1984us=1.984ms??刂萍拇嫫鰽中的UIP標志位用來表示MC146818是否正處于更新周期中,當UIP從0變為1的那個時刻,就表示MC146818將在稍后馬上就開更新周期。在UIP從0變到1的那個時刻與MC146818真正開始Update?Cycle的那個時刻之間時有一段時間間隔的,通常是244us。也就是說,在UIP從0變到1的244us之后,時間與日期寄存器組中的值才會真正開始改變,而在這之間的244us間隔內,它們的值并不會真正改變。如下圖所示:?
(2)get_cmos_time()函數?
該函數只被內核的初始化例程time_init()和內核的APM模塊所調用。其源碼如下:?
/*?not?static:?needed?by?APM?*/?
unsigned?long?get_cmos_time(void)?
{?
unsigned?int?year,?mon,?day,?hour,?min,?sec;?
int?i;?
/*?The?Linux?interpretation?of?the?CMOS?clock?register?contents:?
*?When?the?Update-In-Progress?(UIP)?flag?goes?from?1?to?0,?the?
*?RTC?registers?show?the?second?which?has?precisely?just?started.?
*?Let's?hope?other?operating?systems?interpret?the?RTC?the?same?way.?
*/?
/*?read?RTC?exactly?on?falling?edge?of?update?flag?*/?
for?(i?=?0?;?i?<?1000000?;?i++)?/*?may?take?up?to?1?second...?*/?
if?(CMOS_READ(RTC_FREQ_SELECT)?&?RTC_UIP)?
break;?
for?(i?=?0?;?i?<?1000000?;?i++)?/*?must?try?at?least?2.228?ms?*/?
if?(!(CMOS_READ(RTC_FREQ_SELECT)?&?RTC_UIP))?
break;?
do?{?/*?Isn't?this?overkill???UIP?above?should?guarantee?consistency?*/?
sec?=?CMOS_READ(RTC_SECONDS);?
min?=?CMOS_READ(RTC_MINUTES);?
hour?=?CMOS_READ(RTC_HOURS);?
day?=?CMOS_READ(RTC_DAY_OF_MONTH);?
mon?=?CMOS_READ(RTC_MONTH);?
year?=?CMOS_READ(RTC_YEAR);?
}?while?(sec?!=?CMOS_READ(RTC_SECONDS));?
if?(!(CMOS_READ(RTC_CONTROL)?&?RTC_DM_BINARY)?||?RTC_ALWAYS_BCD)?
{?
BCD_TO_BIN(sec);?
BCD_TO_BIN(min);?
BCD_TO_BIN(hour);?
BCD_TO_BIN(day);?
BCD_TO_BIN(mon);?
BCD_TO_BIN(year);?
}?
if?((year?+=?1900)?<?1970)?
year?+=?100;?
return?mktime(year,?mon,?day,?hour,?min,?sec);?
}?
對該函數的注釋如下:?
(1)在從RTC中讀取時間時,由于RTC存在Update?Cycle,因此軟件發出讀操作的時機是很重要的。對此,get_cmos_time()函數通過UIP標志位來解決這個問題:第一個for循環不停地讀取RTC頻率選擇寄存器中的UIP標志位,并且只要讀到UIP的值為1就馬上退出這個for循環。第二個for循環同樣不停地讀取UIP標志位,但他只要一讀到UIP的值為0就馬上退出這個for循環。這兩個for循環的目的就是要在軟件邏輯上同步RTC的Update?Cycle,顯然第二個for循環最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)?
(2)從第二個for循環退出后,RTC的Update?Cycle已經結束。此時我們就已經把當前時間邏輯定準在RTC的當前一秒時間間隔內。也就是說,這是我們就可以開始從RTC寄存器中讀取當前時間值。但是要注意,讀操作應該保證在244us內完成(準確地說,讀操作要在RTC的下一個更新周期開始之前完成,244us的限制是過分偏執的:-)。所以,get_cmos_time()函數接下來通過CMOS_READ()宏從RTC中依次讀取秒、分鐘、小時、日期、月份和年分。這里的do{}while(sec!=CMOS_READ(RTC_SECOND))循環就是用來確保上述6個讀操作必須在下一個Update?Cycle開始之前完成。?
(3)接下來判定時間的數據格式,PC機中一般總是使用BCD格式的時間,因此需要通過BCD_TO_BIN()宏把BCD格式轉換為二進制格式。?
(4)接下來對年分進行修正,以將年份轉換為“19XX”的格式,如果是1970以前的年份,則將其加上100。?
(5)最后調用mktime()函數將當前時間與日期轉換為相對于1970-01-01?00:00:00的秒數值,并將其作為函數返回值返回。?
函數mktime()定義在include/linux/time.h頭文件中,它用來根據Gauss算法將以year/mon/day/hour/min/sec(如1980-12-31?23:59:59)格式表示的時間轉換為相對于1970-01-01?00:00:00這個UNIX時間基準以來的相對秒數。其源碼如下:?
static?inline?unsigned?long?
mktime?(unsigned?int?year,?unsigned?int?mon,?
unsigned?int?day,?unsigned?int?hour,?
unsigned?int?min,?unsigned?int?sec)?
{?
if?(0?>=?(int)?(mon?-=?2))?{?/*?1..12?->?11,12,1..10?*/?
mon?+=?12;?/*?Puts?Feb?last?since?it?has?leap?day?*/?
year?-=?1;?
}?
return?(((?
(unsigned?long)?(year/4?-?year/100?+?year/400?+?367*mon/12?+?day)?+?
year*365?-?719499?
)*24?+?hour?/*?now?have?hours?*/?
)*60?+?min?/*?now?have?minutes?*/?
)*60?+?sec;?/*?finally?seconds?*/?
}?
(3)set_rtc_mmss()函數?
該函數用來更新RTC中的時間,它僅有一個參數nowtime,是以秒數表示的當前時間,其源碼如下:?
static?int?set_rtc_mmss(unsigned?long?nowtime)?
{?
int?retval?=?0;?
int?real_seconds,?real_minutes,?cmos_minutes;?
unsigned?char?save_control,?save_freq_select;?
/*?gets?recalled?with?irq?locally?disabled?*/?
spin_lock(&rtc_lock);?
save_control?=?CMOS_READ(RTC_CONTROL);?/*?tell?the?clock?it's?being?set?*/?
CMOS_WRITE((save_control|RTC_SET),?RTC_CONTROL);?
save_freq_select?=?CMOS_READ(RTC_FREQ_SELECT);?/*?stop?and?reset?prescaler?*/?
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2),?RTC_FREQ_SELECT);?
cmos_minutes?=?CMOS_READ(RTC_MINUTES);?
if?(!(save_control?&?RTC_DM_BINARY)?||?RTC_ALWAYS_BCD)?
BCD_TO_BIN(cmos_minutes);?
/*?
*?since?we're?only?adjusting?minutes?and?seconds,?
*?don't?interfere?with?hour?overflow.?This?avoids?
*?messing?with?unknown?time?zones?but?requires?your?
*?RTC?not?to?be?off?by?more?than?15?minutes?
*/?
real_seconds?=?nowtime?%?60;?
real_minutes?=?nowtime?/?60;?
if?(((abs(real_minutes?-?cmos_minutes)?+?15)/30)?&?1)?
real_minutes?+=?30;?/*?correct?for?half?hour?time?zone?*/?
real_minutes?%=?60;?
if?(abs(real_minutes?-?cmos_minutes)?<?30)?{?
if?(!(save_control?&?RTC_DM_BINARY)?||?RTC_ALWAYS_BCD)?{?
BIN_TO_BCD(real_seconds);?
BIN_TO_BCD(real_minutes);?
}?
CMOS_WRITE(real_seconds,RTC_SECONDS);?
CMOS_WRITE(real_minutes,RTC_MINUTES);?
}?else?{?
printk(KERN_WARNING?
"set_rtc_mmss:?can't?update?from?%d?to?%d\n",?
cmos_minutes,?real_minutes);?
retval?=?-1;?
}?
/*?The?following?flags?have?to?be?released?exactly?in?this?order,?
*?otherwise?the?DS12887?(popular?MC146818A?clone?with?integrated?
*?battery?and?quartz)?will?not?reset?the?oscillator?and?will?not?
*?update?precisely?500?ms?later.?You?won't?find?this?mentioned?in?
*?the?Dallas?Semiconductor?data?sheets,?but?who?believes?data?
*?sheets?anyway?...?--?Markus?Kuhn?
*/?
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節)。
?dreamice 回復于:2008-11-06 17:56:21
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寄存器時才被使用,后面我們會詳細分析這個函數。
?dreamice 回復于:2008-11-06 17:56:41
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?
*?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-01?00: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)是最為迫切的,因此必須在時鐘中斷服務例程中完成。而其余的幾個要求可以稍緩,因此可以放在時鐘中斷的Bottom?Half中去執行。這樣,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()函數計算時間偏差之用。這一步的實質就是將CPU?TSC寄存器的值更新到內核對TSC的緩存變量last_tsc_low中。?
l?通過讀8254?PIT的通道0的計數器的當前值來計算時間延遲,為此:首先,對自旋鎖i8253_lock進行加鎖。自旋鎖i8253_lock的作用就是用來串行化對8254?PIT的讀寫訪問。其次,向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?accordingly?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存在Update?Cycle,因此最好在一秒時間間隔的中間位置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()函數更新當前進程的時間統計信息。注意,該函數的參數原型是“int?user_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]上。?
l?update_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軟件定時器更新時間間隔。?
l?do_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信號殺死該進程。?
l?do_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;?
}?
}?
l?do_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;?
}?
}
?dreamice 回復于:2008-11-06 17:57:10
7.5?時鐘中斷的Bottom?Half?
與時鐘中斷相關的Bottom?Half向兩主要有兩個:TIMER_BH和TQUEUE_BH。與TIMER_BH相對應的BH函數是timer_bh(),與TQUEUE_BH對應的函數是tqueue_bh()。它們均實現在kernel/timer.c文件中。?
7.5.1?TQUEUE_BH向量?
TQUEUE_BH的作用是用來運行tq_timer這個任務隊列中的任務。因此do_timer()函數僅僅在tq_timer任務隊列不為空的情況才激活TQUEUE_BH向量。函數tqueue_bh()的實現非常簡單,它只是簡單地調用run_task_queue()函數來運行任務隊列tq_timer。如下所示:?
void?tqueue_bh(void)?
{?
run_task_queue(&tq_timer);?
}?
任務對列tq_timer也是定義在kernel/timer.c文件中,如下所示:?
DECLARE_TASK_QUEUE(tq_timer);?
7.5.2?TIMER_BH向量?
TIMER_BH這個Bottom?Half向量是Linux內核時鐘中斷驅動的一個重要輔助部分。內核在每一次對時鐘中斷的服務快要結束時,都會無條件地激活一個TIMER_BH向量,以使得內核在稍后一段延遲后執行相應的BH函數——timer_bh()。該任務的源碼如下:?
void?timer_bh(void)?
{?
update_times();?
run_timer_list();?
}?
從上述源碼可以看出,內核在時鐘中斷驅動的底半部分主要有兩個任務:(1)調用update_times()函數來更新系統全局時間xtime;(2)調用run_timer_list()函數來執行定時器。關于定時器我們將在下一節討論。本節我們主要討論TIMER_BH的第一個任務——對內核時間xtime的更新。?
我們都知道,內核局部時間xtime是用來供用戶程序通過時間syscall來檢索或設置當前系統時間的,而內核代碼在大多數情況下都引用jiffies變量,而很少使用xtime(偶爾也會有引用xtime的情況,比如更新inode的時間標記)。因此,對于時鐘中斷服務程序timer_interrupt()而言,jiffies變量的更新是最緊迫的,而xtime的更新則可以延遲到中斷服務的底半部分來進行。?
由于Bottom?Half機制在執行時間具有某些不確定性,因此在timer_bh()函數得到真正執行之前,期間可能又會有幾次時鐘中斷發生。這樣就會造成時鐘滴答的丟失現象。為了處理這種情況,Linux內核使用了一個輔助全局變量wall_jiffies,來表示上一次更新xtime時的jiffies值。其定義如下(kernel/timer.c):?
/*?jiffies?at?the?most?recent?update?of?wall?time?*/?
unsigned?long?wall_jiffies;?
而timer_bh()函數真正執行時的jiffies值與wall_jiffies的差就是在timer_bh()真正執行之前所發生的時鐘中斷次數。?
函數update_times()的源碼如下(kernel/timer.c):?
static?inline?void?update_times(void)?
{?
unsigned?long?ticks;?
/*?
*?update_times()?is?run?from?the?raw?timer_bh?handler?so?we?
*?just?know?that?the?irqs?are?locally?enabled?and?so?we?don't?
*?need?to?save/restore?the?flags?of?the?local?CPU?here.?-arca?
*/?
write_lock_irq(&xtime_lock);?
ticks?=?jiffies?-?wall_jiffies;?
if?(ticks)?{?
wall_jiffies?+=?ticks;?
update_wall_time(ticks);?
}?
write_unlock_irq(&xtime_lock);?
calc_load(ticks);?
}?
(1)首先,根據jiffies和wall_jiffies的差值計算在此之前一共發生了幾次時鐘滴答,并將這個值保存到局部變量ticks中。并在ticks值大于0的情況下(ticks大于等于1,一般情況下為1):①更新wall_jiffies為jiffies變量的當前值(wall_jiffies+=ticks等價于wall_jiffies=jiffies)。②以參數ticks調用update_wall_time()函數去真正地更新全局時間xtime。?
(2)調用calc_load()函數去計算系統負載情況。這里我們不去深究它。?
函數update_wall_time()函數根據參數ticks所指定的時鐘滴答次數相應地更新內核全局時間變量xtime。其源碼如下(kernel/timer.c):?
/*?
*?Using?a?loop?looks?inefficient,?but?"ticks"?is?
*?usually?just?one?(we?shouldn't?be?losing?ticks,?
*?we're?doing?this?this?way?mainly?for?interrupt?
*?latency?reasons,?not?because?we?think?we'll?
*?have?lots?of?lost?timer?ticks?
*/?
static?void?update_wall_time(unsigned?long?ticks)?
{?
do?{?
ticks--;?
update_wall_time_one_tick();?
}?while?(ticks);?
if?(xtime.tv_usec?>=?1000000)?{?
xtime.tv_usec?-=?1000000;?
xtime.tv_sec++;?
second_overflow();?
}?
}?
對該函數的注釋如下:?
(1)首先,用一個do{}循環來根據參數ticks的值一次一次調用update_wall_time_one_tick()函數來為一次時鐘滴答更新xtime中的tv_usec成員。?
(2)根據需要調整xtime中的秒數成員tv_usec和微秒數成員tv_usec。如果微秒數成員tv_usec的值超過106,則說明已經過了一秒鐘。因此將tv_usec的值減去1000000,并將秒數成員tv_sec的值加1,然后調用second_overflow()函數來處理微秒數成員溢出的情況。?
函數update_wall_time_one_tick()用來更新一次時鐘滴答對系統全局時間xtime的影響。由于tick全局變量表示了一次時鐘滴答的時間間隔長度(以us為單位),因此該函數的實現中最核心的代碼就是將xtime的tv_usec成員增加tick微秒。這里我們不去關心函數實現中與NTP(Network?Time?Protocol)和系統調用adjtimex()的相關部分。其源碼如下(kernel/timer.c):?
/*?in?the?NTP?reference?this?is?called?"hardclock()"?*/?
static?void?update_wall_time_one_tick(void)?
{?
if?(?(time_adjust_step?=?time_adjust)?!=?0?)?{?
/*?We?are?doing?an?adjtime?thing.?
*?
*?Prepare?time_adjust_step?to?be?within?bounds.?
*?Note?that?a?positive?time_adjust?means?we?want?the?clock?
*?to?run?faster.?
*?
*?Limit?the?amount?of?the?step?to?be?in?the?range?
*?-tickadj?..?+tickadj?
*/?
if?(time_adjust?>?tickadj)?
time_adjust_step?=?tickadj;?
else?if?(time_adjust?<?-tickadj)?
time_adjust_step?=?-tickadj;?
/*?Reduce?by?this?step?the?amount?of?time?left?*/?
time_adjust?-=?time_adjust_step;?
}?
xtime.tv_usec?+=?tick?+?time_adjust_step;?
/*?
*?Advance?the?phase,?once?it?gets?to?one?microsecond,?then?
*?advance?the?tick?more.?
*/?
time_phase?+=?time_adj;?
if?(time_phase?<=?-FINEUSEC)?{?
long?ltemp?=?-time_phase?>>?SHIFT_SCALE;?
time_phase?+=?ltemp?<<?SHIFT_SCALE;?
xtime.tv_usec?-=?ltemp;?
}?
else?if?(time_phase?>=?FINEUSEC)?{?
long?ltemp?=?time_phase?>>?SHIFT_SCALE;?
time_phase?-=?ltemp?<<?SHIFT_SCALE;?
xtime.tv_usec?+=?ltemp;?
}?
}
?dreamice 回復于:2008-11-06 17:57:33
7.6?內核定時器機制?
Linux內核2.4版中去掉了老版本內核中的靜態定時器機制,而只留下動態定時器。相應地在timer_bh()函數中也不再通過run_old_timers()函數來運行老式的靜態定時器。動態定時器與靜態定時器這二個概念是相對于Linux內核定時器機制的可擴展功能而言的,動態定時器是指內核的定時器隊列是可以動態變化的,然而就定時器本身而言,二者并無本質的區別??紤]到靜態定時器機制的能力有限,因此Linux內核2.4版中完全去掉了以前的靜態定時器機制。?
7.6.1?Linux內核對定時器的描述?
Linux在include/linux/timer.h頭文件中定義了數據結構timer_list來描述一個內核定時器:?
struct?timer_list?{?
struct?list_head?list;?
unsigned?long?expires;?
unsigned?long?data;?
void?(*function)(unsigned?long);?
};?
各數據成員的含義如下:?
(1)雙向鏈表元素list:用來將多個定時器連接成一條雙向循環隊列。?
(2)expires:指定定時器到期的時間,這個時間被表示成自系統啟動以來的時鐘滴答計數(也即時鐘節拍數)。當一個定時器的expires值小于或等于jiffies變量時,我們就說這個定時器已經超時或到期了。在初始化一個定時器后,通常把它的expires域設置成當前expires變量的當前值加上某個時間間隔值(以時鐘滴答次數計)。?
(3)函數指針function:指向一個可執行函數。當定時器到期時,內核就執行function所指定的函數。而data域則被內核用作function函數的調用參數。?
內核函數init_timer()用來初始化一個定時器。實際上,這個初始化函數僅僅將結構中的list成員初始化為空。如下所示(include/linux/timer.h):?
static?inline?void?init_timer(struct?timer_list?*?timer)?
{?
timer->list.next?=?timer->list.prev?=?NULL;?
}?
由于定時器通常被連接在一個雙向循環隊列中等待執行(此時我們說定時器處于pending狀態)。因此函數time_pending()就可以用list成員是否為空來判斷一個定時器是否處于pending狀態。如下所示(include/linux/timer.h):?
static?inline?int?timer_pending?(const?struct?timer_list?*?timer)?
{?
return?timer->list.next?!=?NULL;?
}?
l?時間比較操作?
在定時器應用中經常需要比較兩個時間值,以確定timer是否超時,所以Linux內核在timer.h頭文件中定義了4個時間關系比較操作宏。這里我們說時刻a在時刻b之后,就意味著時間值a≥b。Linux強烈推薦用戶使用它所定義的下列4個時間比較操作宏(include/linux/timer.h):?
#define?time_after(a,b)?((long)(b)?-?(long)(a)?<?0)?
#define?time_before(a,b)?time_after(b,a)?
#define?time_after_eq(a,b)?((long)(a)?-?(long)(b)?>=?0)?
#define?time_before_eq(a,b)?time_after_eq(b,a)?
7.6.2?動態內核定時器機制的原理?
Linux是怎樣為其內核定時器機制提供動態擴展能力的呢?其關鍵就在于“定時器向量”的概念。所謂“定時器向量”就是指這樣一條雙向循環定時器隊列(對列中的每一個元素都是一個timer_list結構):對列中的所有定時器都在同一個時刻到期,也即對列中的每一個timer_list結構都具有相同的expires值。顯然,可以用一個timer_list結構類型的指針來表示一個定時器向量。?
顯然,定時器expires成員的值與jiffies變量的差值決定了一個定時器將在多長時間后到期。在32位系統中,這個時間差值的最大值應該是0xffffffff。因此如果是基于“定時器向量”基本定義,內核將至少要維護0xffffffff個timer_list結構類型的指針,這顯然是不現實的。?
另一方面,從內核本身這個角度看,它所關心的定時器顯然不是那些已經過期而被執行過的定時器(這些定時器完全可以被丟棄),也不是那些要經過很長時間才會到期的定時器,而是那些當前已經到期或者馬上就要到期的定時器(注意!時間間隔是以滴答次數為計數單位的)。?
基于上述考慮,并假定一個定時器要經過interval個時鐘滴答后才到期(interval=expires-jiffies),則Linux采用了下列思想來實現其動態內核定時器機制:對于那些0≤interval≤255的定時器,Linux嚴格按照定時器向量的基本語義來組織這些定時器,也即Linux內核最關心那些在接下來的255個時鐘節拍內就要到期的定時器,因此將它們按照各自不同的expires值組織成256個定時器向量。而對于那些256≤interval≤0xffffffff的定時器,由于他們離到期還有一段時間,因此內核并不關心他們,而是將它們以一種擴展的定時器向量語義(或稱為“松散的定時器向量語義”)進行組織。所謂“松散的定時器向量語義”就是指:各定時器的expires值可以互不相同的一個定時器隊列。?
具體的組織方案可以分為兩大部分:?
(1)對于內核最關心的、interval值在[0,255]之間的前256個定時器向量,內核是這樣組織它們的:這256個定時器向量被組織在一起組成一個定時器向量數組,并作為數據結構timer_vec_root的一部分,該數據結構定義在kernel/timer.c文件中,如下述代碼段所示:?
/*?
*?Event?timer?code?
*/?
#define?TVN_BITS?6?
#define?TVR_BITS?8?
#define?TVN_SIZE?(1?<<?TVN_BITS)?
#define?TVR_SIZE?(1?<<?TVR_BITS)?
#define?TVN_MASK?(TVN_SIZE?-?1)?
#define?TVR_MASK?(TVR_SIZE?-?1)?
struct?timer_vec?{?
int?index;?
struct?list_head?vec[TVN_SIZE];?
};?
struct?timer_vec_root?{?
int?index;?
struct?list_head?vec[TVR_SIZE];?
};?
static?struct?timer_vec?tv5;?
static?struct?timer_vec?tv4;?
static?struct?timer_vec?tv3;?
static?struct?timer_vec?tv2;?
static?struct?timer_vec_root?tv1;?
static?struct?timer_vec?*?const?tvecs[]?=?{?
(struct?timer_vec?*)&tv1,?&tv2,?&tv3,?&tv4,?&tv5?
};?
#define?NOOF_TVECS?(sizeof(tvecs)?/?sizeof(tvecs[0]))?
基于數據結構timer_vec_root,Linux定義了一個全局變量tv1,以表示內核所關心的前256個定時器向量。這樣內核在處理是否有到期定時器時,它就只從定時器向量數組tv1.vec[256]中的某個定時器向量內進行掃描。而tv1的index字段則指定當前正在掃描定時器向量數組tv1.vec[256]中的哪一個定時器向量,也即該數組的索引,其初值為0,最大值為255(以256為模)。每個時鐘節拍時index字段都會加1。顯然,index字段所指定的定時器向量tv1.vec[index]中包含了當前時鐘節拍內已經到期的所有動態定時器。而定時器向量tv1.vec[index+k]則包含了接下來第k個時鐘節拍時刻將到期的所有動態定時器。當index值又重新變為0時,就意味著內核已經掃描了tv1變量中的所有256個定時器向量。在這種情況下就必須將那些以松散定時器向量語義來組織的定時器向量補充到tv1中來。?
(2)而對于內核不關心的、interval值在[0xff,0xffffffff]之間的定時器,它們的到期緊迫程度也隨其interval值的不同而不同。顯然interval值越小,定時器緊迫程度也越高。因此在將它們以松散定時器向量進行組織時也應該區別對待。通常,定時器的interval值越小,它所處的定時器向量的松散度也就越低(也即向量中的各定時器的expires值相差越小);而interval值越大,它所處的定時器向量的松散度也就越大(也即向量中的各定時器的expires值相差越大)。?
內核規定,對于那些滿足條件:0x100≤interval≤0x3fff的定時器,只要表達式(interval>>8)具有相同值的定時器都將被組織在同一個松散定時器向量中。因此,為組織所有滿足條件0x100≤interval≤0x3fff的定時器,就需要26=64個松散定時器向量。同樣地,為方便起見,這64個松散定時器向量也放在一起形成數組,并作為數據結構timer_vec的一部分?;跀祿Y構timer_vec,Linux定義了全局變量tv2,來表示這64條松散定時器向量。如上述代碼段所示。?
對于那些滿足條件0x4000≤interval≤0xfffff的定時器,只要表達式(interval>>8+6)的值相同的定時器都將被放在同一個松散定時器向量中。同樣,要組織所有滿足條件0x4000≤interval≤0xfffff的定時器,也需要26=64個松散定時器向量。類似地,這64個松散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv3全局變量來表示這64個松散定時器向量。?
對于那些滿足條件0x100000≤interval≤0x3ffffff的定時器,只要表達式(interval>>8+6+6)的值相同的定時器都將被放在同一個松散定時器向量中。同樣,要組織所有滿足條件0x100000≤interval≤0x3ffffff的定時器,也需要26=64個松散定時器向量。類似地,這64個松散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv4全局變量來表示這64個松散定時器向量。?
對于那些滿足條件0x4000000≤interval≤0xffffffff的定時器,只要表達式(interval>>8+6+6+6)的值相同的定時器都將被放在同一個松散定時器向量中。同樣,要組織所有滿足條件0x4000000≤interval≤0xffffffff的定時器,也需要26=64個松散定時器向量。類似地,這64個松散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv5全局變量來表示這64個松散定時器向量。?
最后,為了引用方便,Linux定義了一個指針數組tvecs[],來分別指向tv1、tv2、…、tv5結構變量。如上述代碼所示。?
整個內核定時器機制的總體結構如下圖7-8所示:?
7.6.3?內核動態定時器機制的實現?
在內核動態定時器機制的實現中,有三個操作時非常重要的:(1)將一個定時器插入到它應該所處的定時器向量中。(2)定時器的遷移,也即將一個定時器從它原來所處的定時器向量遷移到另一個定時器向量中。(3)掃描并執行當前已經到期的定時器。?
7.6.3.1?動態定時器機制的初始化?
函數init_timervecs()實現對動態定時器機制的初始化。該函數僅被sched_init()初始化例程所調用。動態定時器機制初始化過程的主要任務就是將tv1、tv2、…、tv5這5個結構變量中的定時器向量指針數組vec[]初始化為NULL。如下所示(kernel/timer.c):?
void?init_timervecs?(void)?
{?
int?i;?
for?(i?=?0;?i?<?TVN_SIZE;?i++)?{?
INIT_LIST_HEAD(tv5.vec?+?i);?
INIT_LIST_HEAD(tv4.vec?+?i);?
INIT_LIST_HEAD(tv3.vec?+?i);?
INIT_LIST_HEAD(tv2.vec?+?i);?
}?
for?(i?=?0;?i?<?TVR_SIZE;?i++)?
INIT_LIST_HEAD(tv1.vec?+?i);?
}?
上述函數中的宏TVN_SIZE是指timer_vec結構類型中的定時器向量指針數組vec[]的大小,值為64。宏TVR_SIZE是指timer_vec_root結構類型中的定時器向量數組vec[]的大小,值為256。?
7.6.3.2?動態定時器的時鐘滴答基準timer_jiffies?
由于動態定時器是在時鐘中斷的Bottom?Half中被執行的,而從TIMER_BH向量被激活到其timer_bh()函數真正執行這段時間內可能會有幾次時鐘中斷發生。因此內核必須記住上一次運行定時器機制是什么時候,也即內核必須保存上一次運行定時器機制時的jiffies值。為此,Linux在kernel/timer.c文件中定義了全局變量timer_jiffies來表示上一次運行定時器機制時的jiffies值。該變量的定義如下所示:?
static?unsigned?long?timer_jiffies;?
7.6.3.3?對內核動態定時器鏈表的保護?
由于內核動態定時器鏈表是一種系統全局共享資源,為了實現對它的互斥訪問,Linux定義了專門的自旋鎖timerlist_lock來保護。任何想要訪問動態定時器鏈表的代碼段都首先必須先持有該自旋鎖,并且在訪問結束后釋放該自旋鎖。其定義如下(kernel/timer.c):?
/*?Initialize?both?explicitly?-?let's?try?to?have?them?in?the?same?cache?line?*/?
spinlock_t?timerlist_lock?=?SPIN_LOCK_UNLOCKED;?
7.6.3.4?將一個定時器插入到鏈表中?
函數add_timer()用來將參數timer指針所指向的定時器插入到一個合適的定時器鏈表中。它首先調用timer_pending()函數判斷所指定的定時器是否已經位于在某個定時器向量中等待執行。如果是,則不進行任何操作,只是打印一條內核告警信息就返回了;如果不是,則調用internal_add_timer()函數完成實際的插入操作。其源碼如下(kernel/timer.c):?
void?add_timer(struct?timer_list?*timer)?
{?
unsigned?long?flags;?
spin_lock_irqsave(&timerlist_lock,?flags);?
if?(timer_pending(timer))?
goto?bug;?
internal_add_timer(timer);?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
return;?
bug:?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
printk("bug:?kernel?timer?added?twice?at?%p.\n",?
__builtin_return_address(0));?
}?
函數internal_add_timer()用于將一個不處于任何定時器向量中的定時器插入到它應該所處的定時器向量中去(根據定時器的expires值來決定)。如下所示(kernel/timer.c):?
static?inline?void?internal_add_timer(struct?timer_list?*timer)?
{?
/*?
*?must?be?cli-ed?when?calling?this?
*/?
unsigned?long?expires?=?timer->expires;?
unsigned?long?idx?=?expires?-?timer_jiffies;?
struct?list_head?*?vec;?
if?(idx?<?TVR_SIZE)?{?
int?i?=?expires?&?TVR_MASK;?
vec?=?tv1.vec?+?i;?
}?else?if?(idx?<?1?<<?(TVR_BITS?+?TVN_BITS))?{?
int?i?=?(expires?>>?TVR_BITS)?&?TVN_MASK;?
vec?=?tv2.vec?+?i;?
}?else?if?(idx?<?1?<<?(TVR_BITS?+?2?*?TVN_BITS))?{?
int?i?=?(expires?>>?(TVR_BITS?+?TVN_BITS))?&?TVN_MASK;?
vec?=?tv3.vec?+?i;?
}?else?if?(idx?<?1?<<?(TVR_BITS?+?3?*?TVN_BITS))?{?
int?i?=?(expires?>>?(TVR_BITS?+?2?*?TVN_BITS))?&?TVN_MASK;?
vec?=?tv4.vec?+?i;?
}?else?if?((signed?long)?idx?<?0)?{?
/*?can?happen?if?you?add?a?timer?with?expires?==?jiffies,?
*?or?you?set?a?timer?to?go?off?in?the?past?
*/?
vec?=?tv1.vec?+?tv1.index;?
}?else?if?(idx?<=?0xffffffffUL)?{?
int?i?=?(expires?>>?(TVR_BITS?+?3?*?TVN_BITS))?&?TVN_MASK;?
vec?=?tv5.vec?+?i;?
}?else?{?
/*?Can?only?get?here?on?architectures?with?64-bit?jiffies?*/?
INIT_LIST_HEAD(&timer->list);?
return;?
}?
/*?
*?Timers?are?FIFO!?
*/?
list_add(&timer->list,?vec->prev);?
}?
對該函數的注釋如下:?
(1)首先,計算定時器的expires值與timer_jiffies的插值(注意!這里應該使用動態定時器自己的時間基準),這個差值就表示這個定時器相對于上一次運行定時器機制的那個時刻還需要多長時間間隔才到期。局部變量idx保存這個差值。?
(2)根據idx的值確定這個定時器應被插入到哪一個定時器向量中。其具體的確定方法我們在7.6.2節已經說過了,這里不再詳述。最后,定時器向量的頭部指針vec表示這個定時器應該所處的定時器向量鏈表頭部。?
(3)最后,調用list_add()函數將定時器插入到vec指針所指向的定時器隊列的尾部。?
7.6.3.5?修改一個定時器的expires值?
當一個定時器已經被插入到內核動態定時器鏈表中后,我們還可以修改該定時器的expires值。函數mod_timer()實現這一點。如下所示(kernel/timer.c):?
int?mod_timer(struct?timer_list?*timer,?unsigned?long?expires)?
{?
int?ret;?
unsigned?long?flags;?
spin_lock_irqsave(&timerlist_lock,?flags);?
timer->expires?=?expires;?
ret?=?detach_timer(timer);?
internal_add_timer(timer);?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
return?ret;?
}?
該函數首先根據參數expires值更新定時器的expires成員。然后調用detach_timer()函數將該定時器從它原來所屬的鏈表中刪除。最后調用internal_add_timer()函數將該定時器根據它新的expires值重新插入到相應的鏈表中。?
函數detach_timer()首先調用timer_pending()來判斷指定的定時器是否已經處于某個鏈表中,如果定時器原來就不處于任何鏈表中,則detach_timer()函數什么也不做,直接返回0值,表示失敗。否則,就調用list_del()函數將定時器從它原來所處的鏈表中摘除。如下所示(kernel/timer.c):?
static?inline?int?detach_timer?(struct?timer_list?*timer)?
{?
if?(!timer_pending(timer))?
return?0;?
list_del(&timer->list);?
return?1;?
}?
7.6.3.6?刪除一個定時器?
函數del_timer()用來將一個定時器從相應的內核定時器隊列中刪除。該函數實際上是對detach_timer()函數的高層封裝。如下所示(kernel/timer.c):?
int?del_timer(struct?timer_list?*?timer)?
{?
int?ret;?
unsigned?long?flags;?
spin_lock_irqsave(&timerlist_lock,?flags);?
ret?=?detach_timer(timer);?
timer->list.next?=?timer->list.prev?=?NULL;?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
return?ret;?
}?
7.6.3.7?定時器遷移操作?
由于一個定時器的interval值會隨著時間的不斷流逝(即jiffies值的不斷增大)而不斷變小,因此那些原本到期緊迫程度較低的定時器會隨著jiffies值的不斷增大而成為既將馬上到期的定時器。比如定時器向量tv2.vec[0]中的定時器在經過256個時鐘滴答后會成為未來256個時鐘滴答內會到期的定時器。因此,定時器在內核動態定時器鏈表中的位置也應相應地隨著改變。改變的規則是:當tv1.index重新變為0時(意味著tv1中的256個定時器向量都已被內核掃描一遍了,從而使tv1中的256個定時器向量變為空),則用tv2.vec[index]定時器向量中的定時器去填充tv1,同時使tv2.index加1(它以64為模)。當tv2.index重新變為0(意味著tv2中的64個定時器向量都已經被全部填充到tv1中去了,從而使得tv2變為空),則用tv3.vec[index]定時器向量中的定時器去填充tv2。如此一直類推下去,直到tv5。?
函數cascade_timers()完成這種定時器遷移操作,該函數只有一個timer_vec結構類型指針的參數tv。這個函數將把定時器向量tv->vec[tv->index]中的所有定時器重新填充到上一層定時器向量中去。如下所示(kernel/timer.c):?
static?inline?void?cascade_timers(struct?timer_vec?*tv)?
{?
/*?cascade?all?the?timers?from?tv?up?one?level?*/?
struct?list_head?*head,?*curr,?*next;?
head?=?tv->vec?+?tv->index;?
curr?=?head->next;?
/*?
*?We?are?removing?_all_?timers?from?the?list,?so?we?don't?have?to?
*?detach?them?individually,?just?clear?the?list?afterwards.?
*/?
while?(curr?!=?head)?{?
struct?timer_list?*tmp;?
tmp?=?list_entry(curr,?struct?timer_list,?list);?
next?=?curr->next;?
list_del(curr);?//?not?needed?
internal_add_timer(tmp);?
curr?=?next;?
}?
INIT_LIST_HEAD(head);?
tv->index?=?(tv->index?+?1)?&?TVN_MASK;?
}?
對該函數的注釋如下:?
(1)首先,用指針head指向定時器頭部向量頭部的list_head結構。指針curr指向定時器向量中的第一個定時器。?
(2)然后,用一個while{}循環來遍歷定時器向量tv->vec[tv->index]。由于定時器向量是一個雙向循環隊列,因此循環的終止條件是curr=head。對于每一個被掃描的定時器,循環體都先調用list_del()函數將當前定時器從鏈表中摘除,然后調用internal_add_timer()函數重新確定該定時器應該被放到哪個定時器向量中去。?
(3)當從while{}循環退出后,定時器向量tv->vec[tv->index]中所有的定時器都已被遷移到其它地方(到它們該呆的地方:-),因此它本身就成為一個空隊列。這里我們顯示地調用INIT_LIST_HEAD()宏來將定時器向量的表頭結構初始化為空。?
(4)最后,將tv->index值加1,當然它是以64為模。?
7.6.4.8?掃描并執行當前已經到期的定時器?
函數run_timer_list()完成這個功能。如前所述,該函數是被timer_bh()函數所調用的,因此內核定時器是在時鐘中斷的Bottom?Half中被執行的。記住這一點非常重要。全局變量timer_jiffies表示了內核上一次執行run_timer_list()函數的時間,因此jiffies與timer_jiffies的差值就表示了自從上一次處理定時器以來,期間一共發生了多少次時鐘中斷,顯然run_timer_list()函數必須為期間所發生的每一次時鐘中斷補上定時器服務。該函數的源碼如下(kernel/timer.c):?
static?inline?void?run_timer_list(void)?
{?
spin_lock_irq(&timerlist_lock);?
while?((long)(jiffies?-?timer_jiffies)?>=?0)?{?
struct?list_head?*head,?*curr;?
if?(!tv1.index)?{?
int?n?=?1;?
do?{?
cascade_timers(tvecs[n]);?
}?while?(tvecs[n]->index?==?1?&&?++n?<?NOOF_TVECS);?
}?
repeat:?
head?=?tv1.vec?+?tv1.index;?
curr?=?head->next;?
if?(curr?!=?head)?{?
struct?timer_list?*timer;?
void?(*fn)(unsigned?long);?
unsigned?long?data;?
timer?=?list_entry(curr,?struct?timer_list,?list);?
fn?=?timer->function;?
data=?timer->data;?
detach_timer(timer);?
timer->list.next?=?timer->list.prev?=?NULL;?
timer_enter(timer);?
spin_unlock_irq(&timerlist_lock);?
fn(data);?
spin_lock_irq(&timerlist_lock);?
timer_exit();?
goto?repeat;?
}?
++timer_jiffies;?
tv1.index?=?(tv1.index?+?1)?&?TVR_MASK;?
}?
spin_unlock_irq(&timerlist_lock);?
}?
函數run_timer_list()的執行過程主要就是用一個大while{}循環來為時鐘中斷執行定時器服務,每一次循環服務一次時鐘中斷。因此一共要執行(jiffies-timer_jiffies+1)次循環。循環體所執行的服務步驟如下:?
(1)首先,判斷tv1.index是否為0,如果為0則需要從tv2中補充定時器到tv1中來。但tv2也可能為空而需要從tv3中補充定時器,因此用一個do{}while循環來調用cascade_timer()函數來依次視需要從tv2中補充tv1,從tv3中補充tv2、…、從tv5中補充tv4。顯然如果tvi.index=0(2≤i≤5),則對于tvi執行cascade_timers()函數后,tvi.index肯定為1。反過來講,如果對tvi執行過cascade_timers()函數后tvi.index不等于1,那么可以肯定在未對tvi執行cascade_timers()函數之前,tvi.index值肯定不為0,因此這時tvi不需要從tv(i+1)中補充定時器,這時就可以終止do{}while循環。?
(2)接下來,就要執行定時器向量tv1.vec[tv1.index]中的所有到期定時器。因此這里用一個goto?repeat循環從頭到尾依次掃描整個定時器對列。由于在執行定時器的關聯函數時并不需要關CPU中斷,所以在用detach_timer()函數將當前定時器從對列中摘除后,就可以調用spin_unlock_irq()函數進行解鎖和開中斷,然后在執行完當前定時器的關聯函數后重新用spin_lock_irq()函數加鎖和關中斷。?
(3)當執行完定時器向量tv1.vec[tv1.index]中的所有到期定時器后,tv1.vec[tv1.index]應該是個空隊列。至此這一次定時器服務也就宣告結束。?
(4)最后,將timer_jiffies值加1,將tv1.index值加1,當然它的模是256。然后,回到while循環開始下一次定時器服務。
?dreamice 回復于:2008-11-06 17:57:51
7.7?進程間隔定時器itimer?
所謂“間隔定時器(Interval?Timer,簡稱itimer)就是指定時器采用“間隔”值(interval)來作為計時方式,當定時器啟動后,間隔值interval將不斷減小。當interval值減到0時,我們就說該間隔定時器到期。與上一節所說的內核動態定時器相比,二者最大的區別在于定時器的計時方式不同。內核定時器是通過它的到期時刻expires值來計時的,當全局變量jiffies值大于或等于內核動態定時器的expires值時,我們說內核內核定時器到期。而間隔定時器則實際上是通過一個不斷減小的計數器來計時的。雖然這兩種定時器并不相同,但卻也是相互聯系的。假如我們每個時鐘節拍都使間隔定時器的間隔計數器減1,那么在這種情形下間隔定時器實際上就是內核動態定時器(下面我們會看到進程的真實間隔定時器就是這樣通過內核定時器來實現的)。?
間隔定時器主要被應用在用戶進程上。每個Linux進程都有三個相互關聯的間隔定時器。其各自的間隔計數器都定義在進程的task_struct結構中,如下所示(include/linux/sched.h):?
struct?task_struct{?
……?
unsigned?long?it_real_value,?it_prof_value,?it_virt_value;?
unsigned?long?it_real_incr,?it_prof_incr,?it_virt_incr;?
struct?timer_list?real_timer;?
……?
}?
(1)真實間隔定時器(ITIMER_REAL):這種間隔定時器在啟動后,不管進程是否運行,每個時鐘滴答都將其間隔計數器減1。當減到0值時,內核向進程發送SIGALRM信號。結構類型task_struct中的成員it_real_incr則表示真實間隔定時器的間隔計數器的初始值,而成員it_real_value則表示真實間隔定時器的間隔計數器的當前值。由于這種間隔定時器本質上與上一節的內核定時器時一樣的,因此Linux實際上是通過real_timer這個內嵌在task_struct結構中的內核動態定時器來實現真實間隔定時器ITIMER_REAL的。?
(2)虛擬間隔定時器ITIMER_VIRT:也稱為進程的用戶態間隔定時器。結構類型task_struct中成員it_virt_incr和it_virt_value分別表示虛擬間隔定時器的間隔計數器的初始值和當前值,二者均以時鐘滴答次數位計數單位。當虛擬間隔定時器啟動后,只有當進程在用戶態下運行時,一次時鐘滴答才能使間隔計數器當前值it_virt_value減1。當減到0值時,內核向進程發送SIGVTALRM信號(虛擬鬧鐘信號),并將it_virt_value重置為初值it_virt_incr。具體請見7.4.3節中的do_it_virt()函數的實現。?
(3)PROF間隔定時器ITIMER_PROF:進程的task_struct結構中的it_prof_value和it_prof_incr成員分別表示PROF間隔定時器的間隔計數器的當前值和初始值(均以時鐘滴答為單位)。當一個進程的PROF間隔定時器啟動后,則只要該進程處于運行中,而不管是在用戶態或核心態下執行,每個時鐘滴答都使間隔計數器it_prof_value值減1。當減到0值時,內核向進程發送SIGPROF信號,并將it_prof_value重置為初值it_prof_incr。具體請見7.4.3節的do_it_prof()函數。?
Linux在include/linux/time.h頭文件中為上述三種進程間隔定時器定義了索引標識,如下所示:?
#define?ITIMER_REAL?0?
#define?ITIMER_VIRTUAL?1?
#define?ITIMER_PROF?2?
7.7.1?數據結構itimerval?
雖然,在內核中間隔定時器的間隔計數器是以時鐘滴答次數為單位,但是讓用戶以時鐘滴答為單位來指定間隔定時器的間隔計數器的初值顯然是不太方便的,因為用戶習慣的時間單位是秒、毫秒或微秒等。所以Linux定義了數據結構itimerval來讓用戶以秒或微秒為單位指定間隔定時器的時間間隔值。其定義如下(include/linux/time.h):?
struct?itimerval?{?
struct?timeval?it_interval;?/*?timer?interval?*/?
struct?timeval?it_value;?/*?current?value?*/?
};?
其中,it_interval成員表示間隔計數器的初始值,而it_value成員表示間隔計數器的當前值。這兩個成員都是timeval結構類型的變量,因此其精度可以達到微秒級。?
l?timeval與jiffies之間的相互轉換?
由于間隔定時器的間隔計數器的內部表示方式與外部表現方式互不相同,因此有必要實現以微秒為單位的timeval結構和為時鐘滴答次數單位的jiffies之間的相互轉換。為此,Linux在kernel/itimer.c中實現了兩個函數實現二者的互相轉換——tvtojiffies()函數和jiffiestotv()函數。它們的源碼如下:?
static?unsigned?long?tvtojiffies(struct?timeval?*value)?
{?
unsigned?long?sec?=?(unsigned)?value->tv_sec;?
unsigned?long?usec?=?(unsigned)?value->tv_usec;?
if?(sec?>?(ULONG_MAX?/?HZ))?
return?ULONG_MAX;?
usec?+=?1000000?/?HZ?-?1;?
usec?/=?1000000?/?HZ;?
return?HZ*sec+usec;?
}?
static?void?jiffiestotv(unsigned?long?jiffies,?struct?timeval?*value)?
{?
value->tv_usec?=?(jiffies?%?HZ)?*?(1000000?/?HZ);?
value->tv_sec?=?jiffies?/?HZ;?
}?
7.7.2?真實間隔定時器ITIMER_REAL的底層運行機制?
間隔定時器ITIMER_VIRT和ITIMER_PROF的底層運行機制是分別通過函數do_it_virt()函數和do_it_prof()函數來實現的,這里就不再重述(可以參見7.4.3節)。?
由于間隔定時器ITIMER_REAL本質上與內核動態定時器并無區別。因此內核實際上是通過內核動態定時器來實現進程的ITIMER_REAL間隔定時器的。為此,task_struct結構中專門設立一個timer_list結構類型的成員變量real_timer。動態定時器real_timer的函數指針function總是被task_struct結構的初始化宏INIT_TASK設置為指向函數it_real_fn()。如下所示(include/linux/sched.h):?
#define?INIT_TASK(tsk)?\?
……?
real_timer:?{?
function:?it_real_fn?\?
}?\?
……?
}?
而real_timer鏈表元素list和data成員總是被進程創建時分別初始化為空和進程task_struct結構的地址,如下所示(kernel/fork.c):?
int?do_fork(……)?
{?
……?
p->it_real_value?=?p->it_virt_value?=?p->it_prof_value?=?0;?
p->it_real_incr?=?p->it_virt_incr?=?p->it_prof_incr?=?0;?
init_timer(&p->real_timer);?
p->real_timer.data?=?(unsigned?long)p;?
……?
}?
當用戶通過setitimer()系統調用來設置進程的ITIMER_REAL間隔定時器時,it_real_incr被設置成非零值,于是該系統調用相應地設置好real_timer.expires值,然后進程的real_timer定時器就被加入到內核動態定時器鏈表中,這樣該進程的ITIMER_REAL間隔定時器就被啟動了。當real_timer定時器到期時,它的關聯函數it_real_fn()將被執行。注意!所有進程的real_timer定時器的function函數指針都指向it_real_fn()這同一個函數,因此it_real_fn()函數必須通過其參數來識別是哪一個進程,為此它將unsigned?long類型的參數p解釋為進程task_struct結構的地址。該函數的源碼如下(kernel/itimer.c):?
void?it_real_fn(unsigned?long?__data)?
{?
struct?task_struct?*?p?=?(struct?task_struct?*)?__data;?
unsigned?long?interval;?
send_sig(SIGALRM,?p,?1);?
interval?=?p->it_real_incr;?
if?(interval)?{?
if?(interval?>?(unsigned?long)?LONG_MAX)?
interval?=?LONG_MAX;?
p->real_timer.expires?=?jiffies?+?interval;?
add_timer(&p->real_timer);?
}?
}?
函數it_real_fn()的執行過程大致如下:?
(1)首先將參數p通過強制類型轉換解釋為進程的task_struct結構類型的指針。?
(2)向進程發送SIGALRM信號。?
(3)在進程的it_real_incr非0的情況下繼續啟動real_timer定時器。首先,計算real_timer定時器的expires值為(jiffies+it_real_incr)。然后,調用add_timer()函數將real_timer加入到內核動態定時器鏈表中。?
7.7.3?itimer定時器的系統調用?
與itimer定時器相關的syscall有兩個:getitimer()和setitimer()。其中,getitimer()用于查詢調用進程的三個間隔定時器的信息,而setitimer()則用來設置調用進程的三個間隔定時器。這兩個syscall都是現在kernel/itimer.c文件中。?
7.7.3.1?getitimer()系統調用的實現?
函數sys_getitimer()有兩個參數:(1)which,指定查詢調用進程的哪一個間隔定時器,其取值可以是ITIMER_REAL、ITIMER_VIRT和ITIMER_PROF三者之一。(2)value指針,指向用戶空間中的一個itimerval結構,用于接收查詢結果。該函數的源碼如下:?
/*?SMP:?Only?we?modify?our?itimer?values.?*/?
asmlinkage?long?sys_getitimer(int?which,?struct?itimerval?*value)?
{?
int?error?=?-EFAULT;?
struct?itimerval?get_buffer;?
if?(value)?{?
error?=?do_getitimer(which,?&get_buffer);?
if?(!error?&&?
copy_to_user(value,?&get_buffer,?sizeof(get_buffer)))?
error?=?-EFAULT;?
}?
return?error;?
}?
顯然,sys_getitimer()函數主要通過do_getitimer()函數來查詢當前進程的間隔定時器信息,并將查詢結果保存在內核空間的結構變量get_buffer中。然后,調用copy_to_usr()宏將get_buffer中結果拷貝到用戶空間緩沖區中。?
函數do_getitimer()的源碼如下(kernel/itimer.c):?
int?do_getitimer(int?which,?struct?itimerval?*value)?
{?
register?unsigned?long?val,?interval;?
switch?(which)?{?
case?ITIMER_REAL:?
interval?=?current->it_real_incr;?
val?=?0;?
/*?
*?FIXME!?This?needs?to?be?atomic,?in?case?the?kernel?timer?happens!?
*/?
if?(timer_pending(¤t->real_timer))?{?
val?=?current->real_timer.expires?-?jiffies;?
/*?look?out?for?negative/zero?itimer..?*/?
if?((long)?val?<=?0)?
val?=?1;?
}?
break;?
case?ITIMER_VIRTUAL:?
val?=?current->it_virt_value;?
interval?=?current->it_virt_incr;?
break;?
case?ITIMER_PROF:?
val?=?current->it_prof_value;?
interval?=?current->it_prof_incr;?
break;?
default:?
return(-EINVAL);?
}?
jiffiestotv(val,?&value->it_value);?
jiffiestotv(interval,?&value->it_interval);?
return?0;?
}?
查詢的過程如下:?
(1)首先,用局部變量val和interval分別表示待查詢間隔定時器的間隔計數器的當前值和初始值。?
(2)如果which=ITIMER_REAL,則查詢當前進程的ITIMER_REAL間隔定時器。于是從current->it_real_incr中得到ITIMER_REAL間隔定時器的間隔計數器的初始值,并將其保存到interval局部變量中。而對于間隔計數器的當前值,由于ITITMER_REAL間隔定時器是通過real_timer這個內核動態定時器來實現的,因此不能通過current->it_real_value來獲得ITIMER_REAL間隔定時器的間隔計數器的當前值,而必須通過real_timer來得到這個值。為此先用timer_pending()函數來判斷current->real_timer是否已被起動。如果未啟動,則說明ITIMER_REAL間隔定時器也未啟動,因此其間隔計數器的當前值肯定是0。因此將val變量簡單地置0就可以了。如果已經啟動,則間隔計數器的當前值應該等于(timer_real.expires-jiffies)。?
(3)如果which=ITIMER_VIRT,則查詢當前進程的ITIMER_VIRT間隔定時器。于是簡單地將計數器初值it_virt_incr和當前值it_virt_value分別保存到局部變量interval和val中。?
(4)如果which=ITIMER_PROF,則查詢當前進程的ITIMER_PROF間隔定時器。于是簡單地將計數器初值it_prof_incr和當前值it_prof_value分別保存到局部變量interval和val中。?
(5)最后,通過轉換函數jiffiestotv()將val和interval轉換成timeval格式的時間值,并保存到value->it_value和value->it_interval中,作為查詢結果返回。?
7.7.3.2?setitimer()系統調用的實現?
函數sys_setitimer()不僅設置調用進程的指定間隔定時器,而且還返回該間隔定時器的原有信息。它有三個參數:(1)which,含義與sys_getitimer()中的參數相同。(2)輸入參數value,指向用戶空間中的一個itimerval結構,含有待設置的新值。(3)輸出參數ovalue,指向用戶空間中的一個itimerval結構,用于接收間隔定時器的原有信息。?
該函數的源碼如下(kernel/itimer.c):?
/*?SMP:?Again,?only?we?play?with?our?itimers,?and?signals?are?SMP?safe?
*?now?so?that?is?not?an?issue?at?all?anymore.?
*/?
asmlinkage?long?sys_setitimer(int?which,?struct?itimerval?*value,?
struct?itimerval?*ovalue)?
{?
struct?itimerval?set_buffer,?get_buffer;?
int?error;?
if?(value)?{?
if(copy_from_user(&set_buffer,?value,?sizeof(set_buffer)))?
return?-EFAULT;?
}?else?
memset((char?*)?&set_buffer,?0,?sizeof(set_buffer));?
error?=?do_setitimer(which,?&set_buffer,?ovalue???&get_buffer?:?0);?
if?(error?||?!ovalue)?
return?error;?
if?(copy_to_user(ovalue,?&get_buffer,?sizeof(get_buffer)))?
return?-EFAULT;?
return?0;?
}?
對該函數的注釋如下:?
(1)在輸入參數指針value非空的情況下,調用copy_from_user()宏將用戶空間中的待設置信息拷貝到內核空間中的set_buffer結構變量中。如果value指針為空,則簡單地將set_buffer結構變量全部置0。?
(2)調用do_setitimer()函數完成實際的設置操作。如果輸出參數ovalue指針有效,則以內核變量get_buffer的地址作為do_setitimer()函數的第三那個調用參數,這樣當do_setitimer()函數返回時,get_buffer結構變量中就將含有當前進程的指定間隔定時器的原來信息。Do_setitimer()函數返回0值表示成功,非0值表示失敗。?
(3)在do_setitimer()函數返回非0值的情況下,或者ovalue指針為空的情況下(不需要輸出間隔定時器的原有信息),函數就可以直接返回了。?
(4)如果ovalue指針非空,調用copy_to_user()宏將get_buffer()結構變量中值拷貝到ovalue所指向的用戶空間中去,以便讓用戶得到指定間隔定時器的原有信息值。?
函數do_setitimer()的源碼如下(kernel/itimer.c):?
int?do_setitimer(int?which,?struct?itimerval?*value,?struct?itimerval?*ovalue)?
{?
register?unsigned?long?i,?j;?
int?k;?
i?=?tvtojiffies(&value->it_interval);?
j?=?tvtojiffies(&value->it_value);?
if?(ovalue?&&?(k?=?do_getitimer(which,?ovalue))?<?0)?
return?k;?
switch?(which)?{?
case?ITIMER_REAL:?
del_timer_sync(¤t->real_timer);?
current->it_real_value?=?j;?
current->it_real_incr?=?i;?
if?(!j)?
break;?
if?(j?>?(unsigned?long)?LONG_MAX)?
j?=?LONG_MAX;?
i?=?j?+?jiffies;?
current->real_timer.expires?=?i;?
add_timer(¤t->real_timer);?
break;?
case?ITIMER_VIRTUAL:?
if?(j)?
j++;?
current->it_virt_value?=?j;?
current->it_virt_incr?=?i;?
break;?
case?ITIMER_PROF:?
if?(j)?
j++;?
current->it_prof_value?=?j;?
current->it_prof_incr?=?i;?
break;?
default:?
return?-EINVAL;?
}?
return?0;?
}?
對該函數的注釋如下:?
(1)首先調用tvtojiffies()函數將timeval格式的初始值和當前值轉換成以時鐘滴答為單位的時間值。并分別保存在局部變量i和j中。?
(2)如果ovalue指針非空,則調用do_getitimer()函數查詢指定間隔定時器的原來信息。如果do_getitimer()函數返回負值,說明出錯。因此就要直接返回錯誤值。否則繼續向下執行開始真正地設置指定的間隔定時器。?
(3)如果which=ITITMER_REAL,表示設置ITIMER_REAL間隔定時器。(a)調用del_timer_sync()函數(該函數在單CPU系統中就是del_timer()函數)將當前進程的real_timer定時器從內核動態定時器鏈表中刪除。(b)將it_real_incr和it_real_value分別設置為局部變量i和j。(c)如果j=0,說明不必啟動real_timer定時器,因此執行break語句退出switch…case控制結構,而直接返回。(d)將real_timer的expires成員設置成(jiffies+當前值j),然后調用add_timer()函數將當前進程的real_timer定時器加入到內核動態定時器鏈表中,從而啟動該定時器。?
(4)如果which=ITIMER_VIRT,則簡單地用局部變量i和j的值分別更新it_virt_incr和it_virt_value就可以了。?
(5)如果which=ITIMER_PROF,則簡單地用局部變量i和j的值分別更新it_prof_incr和it_prof_value就可以了。?
(6)最后,返回0值表示成功。?
7.7.3.3?alarm系統調用?
系統調用alarm可以讓調用進程在指定的秒數間隔后收到一個SIGALRM信號。它只有一個參數seconds,指定以秒數計的定時間隔。函數sys_alarm()的源碼如下(kernel/timer.c):?
/*?
*?For?backwards?compatibility??This?can?be?done?in?libc?so?Alpha?
*?and?all?newer?ports?shouldn't?need?it.?
*/?
asmlinkage?unsigned?long?sys_alarm(unsigned?int?seconds)?
{?
struct?itimerval?it_new,?it_old;?
unsigned?int?oldalarm;?
it_new.it_interval.tv_sec?=?it_new.it_interval.tv_usec?=?0;?
it_new.it_value.tv_sec?=?seconds;?
it_new.it_value.tv_usec?=?0;?
do_setitimer(ITIMER_REAL,?&it_new,?&it_old);?
oldalarm?=?it_old.it_value.tv_sec;?
/*?ehhh..?We?can't?return?0?if?we?have?an?alarm?pending..?*/?
/*?And?we'd?better?return?too?much?than?too?little?anyway?*/?
if?(it_old.it_value.tv_usec)?
oldalarm++;?
return?oldalarm;?
}?
這個系統調用實際上就是啟動進程的ITIMER_REAL間隔定時器。因此它完全可放到用戶空間的C函數庫(比如libc和glibc)中來實現。但是為了保此內核的向后兼容性,2.4.0版的內核仍然將這個syscall放在內核空間中來實現。函數sys_alarm()的實現過程如下:?
(1)根據參數seconds的值構造一個itimerval結構變量it_new。注意!由于alarm啟動的ITIMER_REAL間隔定時器是一次性而不是循環重復的,因此it_new變量中的it_interval成員一定要設置為0。?
(2)調用函數do_setitimer()函數以新構造的定時器it_new來啟動當前進程的ITIMER_REAL定時器,同時將該間隔定時器的原定時間隔保存到局部變量it_old中。?
(3)返回值oldalarm表示以秒數計的ITIMER_REAL間隔定時器的原定時間隔值。因此先把it_old.it_value.tv_sec賦給oldalarm,并且在it_old.it_value.tv_usec非0的情況下,將oldalarm的值加1(也即不足1秒補足1秒)。
?dreamice 回復于:2008-11-06 17:58:13
7.8?時間系統調用的實現?
本節講述與時間相關的syscall,這些系統調用主要用來供用戶進程向內核檢索當前時間與日期,因此他們是內核的時間服務接口。主要的時間系統調用共有5個:time、stime和gettimeofday、settimeofday,以及與網絡時間協議NTP相關的adjtimex系統調用。這里我們不關心NTP,因此僅分析前4個時間系統調用。前4個時間系統調用可以分為兩組:(1)time和stime是一組;(2)gettimeofday和settimeofday是一組。?
7.8.1?系統調用time和stime?
系統調用time()用于獲取以秒數表示的系統當前時間(即內核全局時間變量xtime中的tv_sec成員的值)。它只有一個參數——整型指針tloc,指向用戶空間中的一個整數,用來接收返回的當前時間值。函數sys_time()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_time(int?*?tloc)?
{?
int?i;?
/*?SMP:?This?is?fairly?trivial.?We?grab?CURRENT_TIME?and?
stuff?it?to?user?space.?No?side?effects?*/?
i?=?CURRENT_TIME;?
if?(tloc)?{?
if?(put_user(i,tloc))?
i?=?-EFAULT;?
}?
return?i;?
}?
注釋如下:?
(1)首先,函數調用CURRENT_TIME宏來得到以秒數表示的內核當前時間值,并將該值保存在局部變量i中。宏CURRENT_TIME定義在include/linux/sched.h頭文件中,它實際上就是內核全局時間變量xtime中的tv_sec成員。如下所示:?
#define?CURRENT_TIME?(xtime.tv_sec)?
(2)然后,在參數指針tloc非空的情況下將i的值通過put_user()宏傳遞到有tloc所指向的用戶空間中去,以作為函數的輸出結果。?
(3)最后,將局部變量I的值——也即也秒數表示的系統當前時間值作為返回值返回。?
系統調用stime()與系統調用time()剛好相反,它可以讓用戶設置系統的當前時間(以秒數為單位)。它同樣也只有一個參數——整型指針tptr,指向用戶空間中待設置的時間秒數值。函數sys_stime()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_stime(int?*?tptr)?
{?
int?value;?
if?(!capable(CAP_SYS_TIME))?
return?-EPERM;?
if?(get_user(value,?tptr))?
return?-EFAULT;?
write_lock_irq(&xtime_lock);?
xtime.tv_sec?=?value;?
xtime.tv_usec?=?0;?
time_adjust?=?0;?/*?stop?active?adjtime()?*/?
time_status?|=?STA_UNSYNC;?
time_maxerror?=?NTP_PHASE_LIMIT;?
time_esterror?=?NTP_PHASE_LIMIT;?
write_unlock_irq(&xtime_lock);?
return?0;?
}?
注釋如下:?
(1)首先檢查調用進程的權限,顯然,只有root用戶才能有權限修改系統時間。?
(2)調用get_user()宏將tptr指針所指向的用戶空間中的時間秒數值拷貝到內核空間中來,并保存到局部變量value中。?
(3)將局部變量value的值更新到全局時間變量xtime的tv_sec成員中,并將xtime的tv_usec成員清零。?
(4)在相應地重置其它狀態變量后,函數就可以返回了(返回值0表示成功)。?
7.8.2?系統調用gettimeofday?
這個syscall用來供用戶獲取timeval格式的當前時間信息(精確度為微秒級),以及系統的當前時區信息(timezone)。結構類型timeval的指針參數tv指向接受時間信息的用戶空間緩沖區,參數tz是一個timezone結構類型的指針,指向接收時區信息的用戶空間緩沖區。這兩個參數均為輸出參數,返回值0表示成功,返回負值表示出錯。函數sys_gettimeofday()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_gettimeofday(struct?timeval?*tv,?struct?timezone?*tz)?
{?
if?(tv)?{?
struct?timeval?ktv;?
do_gettimeofday(&ktv);?
if?(copy_to_user(tv,?&ktv,?sizeof(ktv)))?
return?-EFAULT;?
}?
if?(tz)?{?
if?(copy_to_user(tz,?&sys_tz,?sizeof(sys_tz)))?
return?-EFAULT;?
}?
return?0;?
}?
顯然,函數的實現主要分成兩個大的方面:?
(1)如果tv指針有效,則說明用戶要以timeval格式來檢索系統當前時間。為此,先調用do_gettimeofday()函數來檢索系統當前時間并保存到局部變量ktv中。然后再調用copy_to_user()宏將保存在內核空間中的當前時間信息拷貝到由參數指針tv所指向的用戶空間緩沖區中。?
(2)如果tz指針有效,則說明用戶要檢索當前時區信息,因此調用copy_to_user()宏將全局變量sys_tz中的時區信息拷貝到參數指針tz所指向的用戶空間緩沖區中。?
(3)最后,返回0表示成功。?
函數do_gettimeofday()的源碼如下(arch/i386/kernel/time.c):?
/*?
*?This?version?of?gettimeofday?has?microsecond?resolution?
*?and?better?than?microsecond?precision?on?fast?x86?machines?with?TSC.?
*/?
void?do_gettimeofday(struct?timeval?*tv)?
{?
unsigned?long?flags;?
unsigned?long?usec,?sec;?
read_lock_irqsave(&xtime_lock,?flags);?
usec?=?do_gettimeoffset();?
{?
unsigned?long?lost?=?jiffies?-?wall_jiffies;?
if?(lost)?
usec?+=?lost?*?(1000000?/?HZ);?
}?
sec?=?xtime.tv_sec;?
usec?+=?xtime.tv_usec;?
read_unlock_irqrestore(&xtime_lock,?flags);?
while?(usec?>=?1000000)?{?
usec?-=?1000000;?
sec++;?
}?
tv->tv_sec?=?sec;?
tv->tv_usec?=?usec;?
}?
該函數的完成實際的當前時間檢索工作。由于gettimeofday()系統調用要求時間精度要達到微秒級,因此do_gettimeofday()函數不能簡單地返回xtime中的值即可,而必須精確地確定自從時鐘驅動的Bottom?Half上一次更新xtime的那個時刻(由wall_jiffies變量表示,參見7.3節)到do_gettimeofday()函數的當前執行時刻之間的具體時間間隔長度,以便精確地修正xtime的值.如下圖7-9所示:?
假定被do_gettimeofday()用來修正xtime的時間間隔為fixed_usec,而從wall_jiffies到jiffies之間的時間間隔是lost_usec,而從jiffies到do_gettimeofday()函數的執行時刻的時間間隔是offset_usec。則下列三個等式成立:?
fixed_usec=(lost_usec+offset_usec)?
lost_usec=(jiffies-wall_jiffies)*TICK_SIZE=(jiffies-wall_jiffies)*(1000000/HZ)?
由于全局變量last_tsc_low表示上一次時鐘中斷服務函數timer_interrupt()執行時刻的CPU?TSC寄存器的值,因此我們可以用X86?CPU的TSC寄存器來計算offset_usec的值。也即:?
offset_usec=delay_at_last_interrupt+(current_tsc_low-last_tsc_low)*fast_gettimeoffset_quotient?
其中,delay_at_last_interrupt是從上一次發生時鐘中斷到timer_interrupt()服務函數真正執行時刻之間的時間延遲間隔。每一次timer_interrupt()被執行時都會計算這一間隔,并利用TSC的當前值更新last_tsc_low變量(可以參見7.4節)。假定current_tsc_low是do_gettimeofday()函數執行時刻TSC的當前值,全局變量fast_gettimeoffset_quotient則表示TSC寄存器每增加1所代表的時間間隔值,它是由time_init()函數所計算的。?
根據上述原理分析,do_gettimeofday()函數的執行步驟如下:?
(1)調用函數do_gettimeoffset()計算從上一次時鐘中斷發生到執行do_gettimeofday()函數的當前時刻之間的時間間隔offset_usec。?
(2)通過wall_jiffies和jiffies計算lost_usec的值。?
(3)然后,令sec=xtime.tv_sec,usec=xtime.tv_usec+lost_usec+offset_usec。顯然,sec表示系統當前時間在秒數量級上的值,而usec表示系統當前時間在微秒量級上的值。?
(4)用一個while{}循環來判斷usec是否已經溢出而超過106us=1秒。如果溢出,則將usec減去106us并相應地將sec增加1,直到usec不溢出為止。?
(5)最后,用sec和usec分別更新參數指針所指向的timeval結構變量。至此,整個查詢過程結束。?
函數do_gettimeoffset()根據CPU是否配置有TSC寄存器這一條件分別有不同的實現。其定義如下(arch/i386/kernel/time.c):?
#ifndef?CONFIG_X86_TSC?
static?unsigned?long?do_slow_gettimeoffset(void)?
{?
……?
}?
static?unsigned?long?(*do_gettimeoffset)(void)?=?do_slow_gettimeoffset;?
#else?
#define?do_gettimeoffset()?do_fast_gettimeoffset()?
#endif?
顯然,在配置有TSC寄存器的i386平臺上,do_gettimeoffset()函數實際上就是do_fast_gettimeoffset()函數。它通過TSC寄存器來計算do_fast_gettimeoffset()函數被執行的時刻到上一次時鐘中斷發生時的時間間隔值。其源碼如下(arch/i386/kernel/time.c):?
static?inline?unsigned?long?do_fast_gettimeoffset(void)?
{?
register?unsigned?long?eax,?edx;?
/*?Read?the?Time?Stamp?Counter?*/?
rdtsc(eax,edx);?
/*?..?relative?to?previous?jiffy?(32?bits?is?enough)?*/?
eax?-=?last_tsc_low;?/*?tsc_low?delta?*/?
/*?
*?Time?offset?=?(tsc_low?delta)?*?fast_gettimeoffset_quotient?
*?=?(tsc_low?delta)?*?(usecs_per_clock)?
*?=?(tsc_low?delta)?*?(usecs_per_jiffy?/?clocks_per_jiffy)?
*?
*?Using?a?mull?instead?of?a?divl?saves?up?to?31?clock?cycles?
*?in?the?critical?path.?
*/?
__asm__("mull?%2"?
:"=a"?(eax),?"=d"?(edx)?
:"rm"?(fast_gettimeoffset_quotient),?
"0"?(eax));?
/*?our?adjusted?time?offset?in?microseconds?*/?
return?delay_at_last_interrupt?+?edx;?
}?
對該函數的注釋如下:?
(1)先調用rdtsc()函數讀取當前時刻TSC寄存器的值,并將其高32位保存在edx局部變量中,低32位保存在局部變量eax中。?
(2)讓局部變量eax=Δtsc_low=eax-last_tsc_low;也即計算當前時刻的TSC值與上一次時鐘中斷服務函數timer_interrupt()執行時的TSC值之間的差值。?
(3)顯然,從上一次timer_interrupt()到當前時刻的時間間隔就是(Δtsc_low*fast_gettimeoffset_quotient)。因此用一條mul指令來計算這個乘法表達式的值。?
(4)返回值delay_at_last_interrupt+(Δtsc_low*fast_gettimeoffset_quotient)就是從上一次時鐘中斷發生時到當前時刻之間的時間偏移間隔值。?
7.8.3?系統調用settimeofday?
這個系統調用與gettimeofday()剛好相反,它供用戶設置當前時間以及當前時間信息。它也有兩個參數:(1)參數指針tv,指向含有待設置時間信息的用戶空間緩沖區;(2)參數指針tz,指向含有待設置時區信息的用戶空間緩沖區。函數sys_settimeofday()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_settimeofday(struct?timeval?*tv,?struct?timezone?*tz)?
{?
struct?timeval?new_tv;?
struct?timezone?new_tz;?
if?(tv)?{?
if?(copy_from_user(&new_tv,?tv,?sizeof(*tv)))?
return?-EFAULT;?
}?
if?(tz)?{?
if?(copy_from_user(&new_tz,?tz,?sizeof(*tz)))?
return?-EFAULT;?
}?
return?do_sys_settimeofday(tv???&new_tv?:?NULL,?tz???&new_tz?:?NULL);?
}?
函數首先調用copy_from_user()宏將保存在用戶空間中的待設置時間信息和時區信息拷貝到內核空間中來,并保存到局部變量new_tv和new_tz中。然后,調用do_sys_settimeofday()函數完成實際的時間設置和時區設置操作。?
函數do_sys_settimeofday()的源碼如下(kernel/time.c):?
int?do_sys_settimeofday(struct?timeval?*tv,?struct?timezone?*tz)?
{?
static?int?firsttime?=?1;?
if?(!capable(CAP_SYS_TIME))?
return?-EPERM;?
if?(tz)?{?
/*?SMP?safe,?global?irq?locking?makes?it?work.?*/?
sys_tz?=?*tz;?
if?(firsttime)?{?
firsttime?=?0;?
if?(!tv)?
warp_clock();?
}?
}?
if?(tv)?
{?
/*?SMP?safe,?again?the?code?in?arch/foo/time.c?should?
*?globally?block?out?interrupts?when?it?runs.?
*/?
do_settimeofday(tv);?
}?
return?0;?
}?
該函數的執行過程如下:?
(1)首先,檢查調用進程是否有相應的權限。如果沒有,則返回錯誤值-EPERM。?
(2)如果執政tz有效,則用tz所指向的新時區信息更新全局變量sys_tz。并且如果是第一次設置時區信息,則在tv指針不為空的情況下調用wrap_clock()函數來調整xtime中的秒數值。函數wrap_clock()的源碼如下(kernel/time.c):?
inline?static?void?warp_clock(void)?
{?
write_lock_irq(&xtime_lock);?
xtime.tv_sec?+=?sys_tz.tz_minuteswest?*?60;?
write_unlock_irq(&xtime_lock);?
}?
(3)如果參數tv指針有效,則根據tv所指向的新時間信息調用do_settimeofday()函數來更新內核的當前時間xtime。?
(4)最后,返回0值表示成功。?
函數do_settimeofday()執行剛好與do_gettimeofday()相反的操作。這是因為全局變量xtime所表示的時間是與wall_jiffies相對應的那一個時刻。因此,必須從參數指針tv所指向的新時間中減去時間間隔fixed_usec(其含義見7.8.2節)。函數源碼如下(arch/i386/kernel/time.c):?
void?do_settimeofday(struct?timeval?*tv)?
{?
write_lock_irq(&xtime_lock);?
/*?
*?This?is?revolting.?We?need?to?set?"xtime"?correctly.?However,?the?
*?value?in?this?location?is?the?value?at?the?most?recent?update?of?
*?wall?time.?Discover?what?correction?gettimeofday()?would?have?
*?made,?and?then?undo?it!?
*/?
tv->tv_usec?-=?do_gettimeoffset();?
tv->tv_usec?-=?(jiffies?-?wall_jiffies)?*?(1000000?/?HZ);?
while?(tv->tv_usec?<?0)?{?
tv->tv_usec?+=?1000000;?
tv->tv_sec--;?
}?
xtime?=?*tv;?
time_adjust?=?0;?/*?stop?active?adjtime()?*/?
time_status?|=?STA_UNSYNC;?
time_maxerror?=?NTP_PHASE_LIMIT;?
time_esterror?=?NTP_PHASE_LIMIT;?
write_unlock_irq(&xtime_lock);?
}?
該函數的執行步驟如下:?
(1)調用do_gettimeoffset()函數計算上一次時鐘中斷發生時刻到當前時刻之間的時間間隔值。?
(2)通過wall_jiffies與jiffies計算二者之間的時間間隔lost_usec。?
(3)從tv->tv_usec中減去fixed_usec,即:tv->tv_usec-=(lost_usec+offset_usec)。?
(4)用一個while{}循環根據tv->tv_usec是否小于0來調整tv結構變量。如果tv->tv_usec小于0,則將tv->tv_usec加上106us,并相應地將tv->tv_sec減1。直到tv->tv_usec不小于0為止。?
(5)用修正后的時間tv來更新內核全局時間變量xtime。?
(6)最后,重置其它時間狀態變量。?
至此,我們已經完全分析了整個Linux內核的時鐘機制!
時間在一個操作系統內核中占據著重要的地位,它是驅動一個OS內核運行的“起博器”。一般說來,內核主要需要兩種類型的時間:?
1.?在內核運行期間持續記錄當前的時間與日期,以便內核對某些對象和事件作時間標記(timestamp,也稱為“時間戳”),或供用戶通過時間syscall進行檢索。?
2.?維持一個固定周期的定時器,以提醒內核或用戶一段時間已經過去了。?
PC機中的時間是有三種時鐘硬件提供的,而這些時鐘硬件又都基于固定頻率的晶體振蕩器來提供時鐘方波信號輸入。這三種時鐘硬件是:(1)實時時鐘(Real?Time?Clock,RTC);(2)可編程間隔定時器(Programmable?Interval?Timer,PIT);(3)時間戳計數器(Time?Stamp?Counter,TSC)。?
7.1?時鐘硬件?
7.1.1?實時時鐘RTC?
自從IBM?PC?AT起,所有的PC機就都包含了一個叫做實時時鐘(RTC)的時鐘芯片,以便在PC機斷電后仍然能夠繼續保持時間。顯然,RTC是通過主板上的電池來供電的,而不是通過PC機電源來供電的,因此當PC機關掉電源后,RTC仍然會繼續工作。通常,CMOS?RAM和RTC被集成到一塊芯片上,因此RTC也稱作“CMOS?Timer”。最常見的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容于MC146818,并有一定的擴展。本節內容主要基于MC146818這一標準的RTC芯片。具體內容可以參考MC146818的Datasheet。?
7.1.1.1?RTC寄存器?
MC146818?RTC芯片一共有64個寄存器。它們的芯片內部地址編號為0x00~0x3F(不是I/O端口地址),這些寄存器一共可以分為三組:?
(1)時鐘與日歷寄存器組:共有10個(0x00~0x09),表示時間、日歷的具體信息。在PC機中,這些寄存器中的值都是以BCD格式來存儲的(比如23dec=0x23BCD)。?
(2)狀態和控制寄存器組:共有4個(0x0A~0x0D),控制RTC芯片的工作方式,并表示當前的狀態。?
(3)CMOS配置數據:通用的CMOS?RAM,它們與時間無關,因此我們不關心它。?
時鐘與日歷寄存器組的詳細解釋如下:?
Address?Function?
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(01=Sunday)?
07?Current?date?of?month?
08?Current?month?
09?Current?year(final?two?digits,eg:93)?
狀態寄存器A(地址0x0A)的格式如下:?
其中:?
(1)bit[7]——UIP標志(Update?in?Progress),為1表示RTC正在更新日歷寄存器組中的值,此時日歷寄存器組是不可訪問的(此時訪問它們將得到一個無意義的漸變值)。?
(2)bit[6:4]——這三位是“除法器控制位”(divider-control?bits),用來定義RTC的操作頻率。各種可能的值如下:?
Divider?bits?Time-base?frequency?Divider?Reset?Operation?Mode?
DV2?DV1?DV0?
0?0?0?4.194304?MHZ?NO?YES?
0?0?1?1.048576?MHZ?NO?YES?
0?1?0?32.769?KHZ?NO?YES?
1?1?0/1?任何?YES?NO?
PC機通常將Divider?bits設置成“010”。?
(3)bit[3:0]——速率選擇位(Rate?Selection?bits),用于周期性或方波信號輸出。?
RS?bits?4.194304或1.048578?MHZ?32.768?KHZ?
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”。?
狀態寄存器B的格式如下所示:?
各位的含義如下:?
(1)bit[7]——SET標志。為1表示RTC的所有更新過程都將終止,用戶程序隨后馬上對日歷寄存器組中的值進行初始化設置。為0表示將允許更新過程繼續。?
(2)bit[6]——PIE標志,周期性中斷使能標志。?
(3)bit[5]——AIE標志,告警中斷使能標志。?
(4)bit[4]——UIE標志,更新結束中斷使能標志。?
(5)bit[3]——SQWE標志,方波信號使能標志。?
(6)bit[2]——DM標志,用來控制日歷寄存器組的數據模式,0=BCD,1=BINARY。BIOS總是將它設置為0。?
(7)bit[1]——24/12標志,用來控制hour寄存器,0表示12小時制,1表示24小時制。PC機BIOS總是將它設置為1。?
(8)bit[0]——DSE標志。BIOS總是將它設置為0。?
狀態寄存器C的格式如下:?
(1)bit[7]——IRQF標志,中斷請求標志,當該位為1時,說明寄存器B中斷請求發生。?
(2)bit[6]——PF標志,周期性中斷標志,為1表示發生周期性中斷請求。?
(3)bit[5]——AF標志,告警中斷標志,為1表示發生告警中斷請求。?
(4)bit[4]——UF標志,更新結束中斷標志,為1表示發生更新結束中斷請求。?
狀態寄存器D的格式如下:?
(1)bit[7]——VRT標志(Valid?RAM?and?Time),為1表示OK,為0表示RTC已經掉電。?
(2)bit[6:0]——總是為0,未定義。?
7.1.1.2?通過I/O端口訪問RTC?
在PC機中可以通過I/O端口0x70和0x71來讀寫RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是數據端口。?
讀RTC芯片寄存器的步驟是:?
mov?al,?addr?
out?70h,?al?;?Select?reg_addr?in?RTC?chip?
jmp?$+2?;?a?slight?delay?to?settle?thing?
in?al,?71h?;?
寫RTC寄存器的步驟如下:?
mov?al,?addr?
out?70h,?al?;?Select?reg_addr?in?RTC?chip?
jmp?$+2?;?a?slight?delay?to?settle?thing?
mov?al,?value?
out?71h,?al?
7.1.2?可編程間隔定時器PIT?
每個PC機中都有一個PIT,以通過IRQ0產生周期性的時鐘中斷信號。當前使用最普遍的是Intel?8254?PIT芯片,它的I/O端口地址是0x40~0x43。?
Intel?8254?PIT有3個計時通道,每個通道都有其不同的用途:?
(1)?通道0用來負責更新系統時鐘。每當一個時鐘滴答過去時,它就會通過IRQ0向系統產生一次時鐘中斷。?
(2)?通道1通常用于控制DMAC對RAM的刷新。?
(3)?通道2被連接到PC機的揚聲器,以產生方波信號。?
每個通道都有一個向下減小的計數器,8254?PIT的輸入時鐘信號的頻率是1193181HZ,也即一秒鐘輸入1193181個clock-cycle。每輸入一個clock-cycle其時間通道的計數器就向下減1,一直減到0值。因此對于通道0而言,當他的計數器減到0時,PIT就向系統產生一次時鐘中斷,表示一個時鐘滴答已經過去了。當各通道的計數器減到0時,我們就說該通道處于“Terminal?count”狀態。?
通道計數器的最大值是10000h,所對應的時鐘中斷頻率是1193181/(65536)=18.2HZ,也就是說,此時一秒鐘之內將產生18.2次時鐘中斷。?
7.1.2.1?PIT的I/O端口?
在i386平臺上,8254芯片的各寄存器的I/O端口地址如下:?
Port?Description?
40h?Channel?0?counter(read/write)?
41h?Channel?1?counter(read/write)?
42h?Channel?2?counter(read/write)?
43h?PIT?control?word(write?only)?
其中,由于通道0、1、2的計數器是一個16位寄存器,而相應的端口卻都是8位的,因此讀寫通道計數器必須進行進行兩次I/O端口讀寫操作,分別對應于計數器的高字節和低字節,至于是先讀寫高字節再讀寫低字節,還是先讀寫低字節再讀寫高字節,則由PIT的控制寄存器來決定。8254?PIT的控制寄存器的格式如下:?
(1)bit[7:6]——Select?Counter,選擇對那個計數器進行操作?!?0”表示選擇Counter?0,“01”表示選擇Counter?1,“10”表示選擇Counter?2,“11”表示Read-Back?Command(僅對于8254,對于8253無效)。?
(2)bit[5:4]——Read/Write/Latch格式位?!?0”表示鎖存(Latch)當前計數器的值;“01”只讀寫計數器的高字節(MSB);“10”只讀寫計數器的低字節(LSB);“11”表示先讀寫計數器的LSB,再讀寫MSB。?
(3)bit[3:1]——Mode?bits,控制各通道的工作模式。“000”對應Mode?0;“001”對應Mode?1;“010”對應Mode?2;“011”對應Mode?3;“100”對應Mode?4;“101”對應Mode?5。?
(4)bit[0]——控制計數器的存儲模式。0表示以二進制格式存儲,1表示計數器中的值以BCD格式存儲。?
7.1.2.2?PIT通道的工作模式?
PIT各通道可以工作在下列6種模式下:?
1.?Mode?0:當通道處于“Terminal?count”狀態時產生中斷信號。?
2.?Mode?1:Hardware?retriggerable?one-shot。?
3.?Mode?2:Rate?Generator。這種模式典型地被用來產生實時時鐘中斷。此時通道的信號輸出管腳OUT初始時被設置為高電平,并以此持續到計數器的值減到1。然后在接下來的這個clock-cycle期間,OUT管腳將變為低電平,直到計數器的值減到0。當計數器的值被自動地重新加載后,OUT管腳又變成高電平,然后重復上述過程。通道0通常工作在這個模式下。?
4.?Mode?3:方波信號發生器。?
5.?Mode?4:Software?triggered?strobe。?
6.?Mode?5:Hardware?triggered?strobe。?
7.1.2.3?鎖存計數器(Latch?Counter)?
當控制寄存器中的bit[5:4]設置成0時,將把當前通道的計數器值鎖存。此時通過I/O端口可以讀到一個穩定的計數器值,因為計數器表面上已經停止向下計數(PIT芯片內部并沒有停止向下計數)。NOTE!一旦發出了鎖存命令,就要馬上讀計數器的值。?
7.1.3?時間戳記數器TSC?
從Pentium開始,所有的Intel?80x86?CPU就都又包含一個64位的時間戳記數器(TSC)的寄存器。該寄存器實際上是一個不斷增加的計數器,它在CPU的每個時鐘信號到來時加1(也即每一個clock-cycle輸入CPU時,該計數器的值就加1)。?
匯編指令rdtsc可以用于讀取TSC的值。利用CPU的TSC,操作系統通??梢缘玫礁鼮榫珳实臅r間度量。假如clock-cycle的頻率是400MHZ,那么TSC就將每2.5納秒增加一次。
?dreamice 回復于:2008-11-06 17:56:02
.2?Linux內核對RTC的編程?
MC146818?RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上產生周期性的中斷,中斷的頻率在2HZ~8192HZ之間。與MC146818?RTC對應的設備驅動程序實現在include/linux/rtc.h和drivers/char/rtc.c文件中,對應的設備文件是/dev/rtc(major=10,minor=135,只讀字符設備)。因此用戶進程可以通過對她進行編程以使得當RTC到達某個特定的時間值時激活IRQ8線,從而將RTC當作一個鬧鐘來用。?
而Linux內核對RTC的唯一用途就是把RTC用作“離線”或“后臺”的時間與日期維護器。當Linux內核啟動時,它從RTC中讀取時間與日期的基準值。然后再運行期間內核就完全拋開RTC,從而以軟件的形式維護系統的當前時間與日期,并在需要時將時間回寫到RTC芯片中。?
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h頭文件中分別定義了mc146818?RTC芯片各寄存器的含義以及RTC芯片在i386平臺上的I/O端口操作。而通用的RTC接口則聲明在include/linux/rtc.h頭文件中。?
7.2.1?RTC芯片的I/O端口操作?
Linux在include/asm-i386/mc146818rtc.h頭文件中定義了RTC芯片的I/O端口操作。端口0x70被稱為“RTC端口0”,端口0x71被稱為“RTC端口1”,如下所示:?
#ifndef?RTC_PORT?
#define?RTC_PORT(x)?(0x70?+?(x))?
#define?RTC_ALWAYS_BCD?1?/*?RTC?operates?in?binary?mode?*/?
#endif?
顯然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。?
端口0x70被用作RTC芯片內部寄存器的地址索引端口,而端口0x71則被用作RTC芯片內部寄存器的數據端口。再讀寫一個RTC寄存器之前,必須先把該寄存器在RTC芯片內部的地址索引值寫到端口0x70中。根據這一點,讀寫一個RTC寄存器的宏定義CMOS_READ()和CMOS_WRITE()如下:?
#define?CMOS_READ(addr)?({?\?
outb_p((addr),RTC_PORT(0));?\?
inb_p(RTC_PORT(1));?\?
})?
#define?CMOS_WRITE(val,?addr)?({?\?
outb_p((addr),RTC_PORT(0));?\?
outb_p((val),RTC_PORT(1));?\?
})?
#define?RTC_IRQ?8?
在上述宏定義中,參數addr是RTC寄存器在芯片內部的地址值,取值范圍是0x00~0x3F,參數val是待寫入寄存器的值。宏RTC_IRQ是指RTC芯片所連接的中斷請求輸入線號,通常是8。?
7.2.2?對RTC寄存器的定義?
Linux在include/linux/mc146818rtc.h這個頭文件中定義了RTC各寄存器的含義。?
(1)寄存器內部地址索引的定義?
Linux內核僅使用RTC芯片的時間與日期寄存器組和控制寄存器組,地址為0x00~0x09之間的10個時間與日期寄存器的定義如下:?
#define?RTC_SECONDS?0?
#define?RTC_SECONDS_ALARM?1?
#define?RTC_MINUTES?2?
#define?RTC_MINUTES_ALARM?3?
#define?RTC_HOURS?4?
#define?RTC_HOURS_ALARM?5?
/*?RTC_*_alarm?is?always?true?if?2?MSBs?are?set?*/?
#?define?RTC_ALARM_DONT_CARE?0xC0?
#define?RTC_DAY_OF_WEEK?6?
#define?RTC_DAY_OF_MONTH?7?
#define?RTC_MONTH?8?
#define?RTC_YEAR?9?
四個控制寄存器的地址定義如下:?
#define?RTC_REG_A?10?
#define?RTC_REG_B?11?
#define?RTC_REG_C?12?
#define?RTC_REG_D?13?
(2)各控制寄存器的狀態位的詳細定義?
控制寄存器A(0x0A)主要用于選擇RTC芯片的工作頻率,因此也稱為RTC頻率選擇寄存器。因此Linux用一個宏別名RTC_FREQ_SELECT來表示控制寄存器A,如下:?
#define?RTC_FREQ_SELECT?RTC_REG_A?
RTC頻率寄存器中的位被分為三組:①bit[7]表示UIP標志;②bit[6:4]用于除法器的頻率選擇;③bit[3:0]用于速率選擇。它們的定義如下:?
#?define?RTC_UIP?0x80?
#?define?RTC_DIV_CTL?0x70?
/*?Periodic?intr.?/?Square?wave?rate?select.?0=none,?1=32.8kHz,...?15=2Hz?*/?
#?define?RTC_RATE_SELECT?0x0F?
正如7.1.1.1節所介紹的那樣,bit[6:4]有5中可能的取值,分別為除法器選擇不同的工作頻率或用于重置除法器,各種可能的取值如下定義所示:?
/*?divider?control:?refclock?values?4.194?/?1.049?MHz?/?32.768?kHz?*/?
#?define?RTC_REF_CLCK_4MHZ?0x00?
#?define?RTC_REF_CLCK_1MHZ?0x10?
#?define?RTC_REF_CLCK_32KHZ?0x20?
/*?2?values?for?divider?stage?reset,?others?for?"testing?purposes?only"?*/?
#?define?RTC_DIV_RESET1?0x60?
#?define?RTC_DIV_RESET2?0x70?
寄存器B中的各位用于使能/禁止RTC的各種特性,因此控制寄存器B(0x0B)也稱為“控制寄存器”,Linux用宏別名RTC_CONTROL來表示控制寄存器B,它與其中的各標志位的定義如下所示:?
#define?RTC_CONTROL?RTC_REG_B?
#?define?RTC_SET?0x80?/*?disable?updates?for?clock?setting?*/?
#?define?RTC_PIE?0x40?/*?periodic?interrupt?enable?*/?
#?define?RTC_AIE?0x20?/*?alarm?interrupt?enable?*/?
#?define?RTC_UIE?0x10?/*?update-finished?interrupt?enable?*/?
#?define?RTC_SQWE?0x08?/*?enable?square-wave?output?*/?
#?define?RTC_DM_BINARY?0x04?/*?all?time/date?values?are?BCD?if?clear?*/?
#?define?RTC_24H?0x02?/*?24?hour?mode?-?else?hours?bit?7?means?pm?*/?
#?define?RTC_DST_EN?0x01?/*?auto?switch?DST?-?works?f.?USA?only?*/?
寄存器C是RTC芯片的中斷請求狀態寄存器,Linux用宏別名RTC_INTR_FLAGS來表示寄存器C,它與其中的各標志位的定義如下所示:?
#define?RTC_INTR_FLAGS?RTC_REG_C?
/*?caution?-?cleared?by?read?*/?
#?define?RTC_IRQF?0x80?/*?any?of?the?following?3?is?active?*/?
#?define?RTC_PF?0x40?
#?define?RTC_AF?0x20?
#?define?RTC_UF?0x10?
寄存器D僅定義了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也稱為RTC的有效寄存器。Linux用宏別名RTC_VALID來表示寄存器D,如下:?
#define?RTC_VALID?RTC_REG_D?
#?define?RTC_VRT?0x80?/*?valid?RAM?and?time?*/?
(3)二進制格式與BCD格式的相互轉換?
由于時間與日期寄存器中的值可能以BCD格式存儲,也可能以二進制格式存儲,因此需要定義二進制格式與BCD格式之間的相互轉換宏,以方便編程。如下:?
#ifndef?BCD_TO_BIN?
#define?BCD_TO_BIN(val)?((val)=((val)&15)?+?((val)>>4)*10)?
#endif?
#ifndef?BIN_TO_BCD?
#define?BIN_TO_BCD(val)?((val)=(((val)/10)<<4)?+?(val)%10)?
#endif?
7.2.3?內核對RTC的操作?
如前所述,Linux內核與RTC進行互操作的時機只有兩個:(1)內核在啟動時從RTC中讀取啟動時的時間與日期;(2)內核在需要時將時間與日期回寫到RTC中。為此,Linux內核在arch/i386/kernel/time.c文件中實現了函數get_cmos_time()來進行對RTC的第一種操作。顯然,get_cmos_time()函數僅僅在內核啟動時被調用一次。而對于第二種操作,Linux則同樣在arch/i386/kernel/time.c文件中實現了函數set_rtc_mmss(),以支持向RTC中回寫當前時間與日期。下面我們將來分析這二個函數的實現。?
在分析get_cmos_time()函數之前,我們先來看看RTC芯片對其時間與日期寄存器組的更新原理。?
(1)Update?In?Progress?
當控制寄存器B中的SET標志位為0時,MC146818芯片每秒都會在芯片內部執行一個“更新周期”(Update?Cycle),其作用是增加秒寄存器的值,并檢查秒寄存器是否溢出。如果溢出,則增加分鐘寄存器的值,如此一致下去直到年寄存器。在“更新周期”期間,時間與日期寄存器組(0x00~0x09)是不可用的,此時如果讀取它們的值將得到未定義的值,因為MC146818在整個更新周期期間會把時間與日期寄存器組從CPU總線上脫離,從而防止軟件程序讀到一個漸變的數據。?
在MC146818的輸入時鐘頻率(也即晶體增蕩器的頻率)為4.194304MHZ或1.048576MHZ的情況下,“更新周期”需要花費248us,而對于輸入時鐘頻率為32.768KHZ的情況,“更新周期”需要花費1984us=1.984ms??刂萍拇嫫鰽中的UIP標志位用來表示MC146818是否正處于更新周期中,當UIP從0變為1的那個時刻,就表示MC146818將在稍后馬上就開更新周期。在UIP從0變到1的那個時刻與MC146818真正開始Update?Cycle的那個時刻之間時有一段時間間隔的,通常是244us。也就是說,在UIP從0變到1的244us之后,時間與日期寄存器組中的值才會真正開始改變,而在這之間的244us間隔內,它們的值并不會真正改變。如下圖所示:?
(2)get_cmos_time()函數?
該函數只被內核的初始化例程time_init()和內核的APM模塊所調用。其源碼如下:?
/*?not?static:?needed?by?APM?*/?
unsigned?long?get_cmos_time(void)?
{?
unsigned?int?year,?mon,?day,?hour,?min,?sec;?
int?i;?
/*?The?Linux?interpretation?of?the?CMOS?clock?register?contents:?
*?When?the?Update-In-Progress?(UIP)?flag?goes?from?1?to?0,?the?
*?RTC?registers?show?the?second?which?has?precisely?just?started.?
*?Let's?hope?other?operating?systems?interpret?the?RTC?the?same?way.?
*/?
/*?read?RTC?exactly?on?falling?edge?of?update?flag?*/?
for?(i?=?0?;?i?<?1000000?;?i++)?/*?may?take?up?to?1?second...?*/?
if?(CMOS_READ(RTC_FREQ_SELECT)?&?RTC_UIP)?
break;?
for?(i?=?0?;?i?<?1000000?;?i++)?/*?must?try?at?least?2.228?ms?*/?
if?(!(CMOS_READ(RTC_FREQ_SELECT)?&?RTC_UIP))?
break;?
do?{?/*?Isn't?this?overkill???UIP?above?should?guarantee?consistency?*/?
sec?=?CMOS_READ(RTC_SECONDS);?
min?=?CMOS_READ(RTC_MINUTES);?
hour?=?CMOS_READ(RTC_HOURS);?
day?=?CMOS_READ(RTC_DAY_OF_MONTH);?
mon?=?CMOS_READ(RTC_MONTH);?
year?=?CMOS_READ(RTC_YEAR);?
}?while?(sec?!=?CMOS_READ(RTC_SECONDS));?
if?(!(CMOS_READ(RTC_CONTROL)?&?RTC_DM_BINARY)?||?RTC_ALWAYS_BCD)?
{?
BCD_TO_BIN(sec);?
BCD_TO_BIN(min);?
BCD_TO_BIN(hour);?
BCD_TO_BIN(day);?
BCD_TO_BIN(mon);?
BCD_TO_BIN(year);?
}?
if?((year?+=?1900)?<?1970)?
year?+=?100;?
return?mktime(year,?mon,?day,?hour,?min,?sec);?
}?
對該函數的注釋如下:?
(1)在從RTC中讀取時間時,由于RTC存在Update?Cycle,因此軟件發出讀操作的時機是很重要的。對此,get_cmos_time()函數通過UIP標志位來解決這個問題:第一個for循環不停地讀取RTC頻率選擇寄存器中的UIP標志位,并且只要讀到UIP的值為1就馬上退出這個for循環。第二個for循環同樣不停地讀取UIP標志位,但他只要一讀到UIP的值為0就馬上退出這個for循環。這兩個for循環的目的就是要在軟件邏輯上同步RTC的Update?Cycle,顯然第二個for循環最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)?
(2)從第二個for循環退出后,RTC的Update?Cycle已經結束。此時我們就已經把當前時間邏輯定準在RTC的當前一秒時間間隔內。也就是說,這是我們就可以開始從RTC寄存器中讀取當前時間值。但是要注意,讀操作應該保證在244us內完成(準確地說,讀操作要在RTC的下一個更新周期開始之前完成,244us的限制是過分偏執的:-)。所以,get_cmos_time()函數接下來通過CMOS_READ()宏從RTC中依次讀取秒、分鐘、小時、日期、月份和年分。這里的do{}while(sec!=CMOS_READ(RTC_SECOND))循環就是用來確保上述6個讀操作必須在下一個Update?Cycle開始之前完成。?
(3)接下來判定時間的數據格式,PC機中一般總是使用BCD格式的時間,因此需要通過BCD_TO_BIN()宏把BCD格式轉換為二進制格式。?
(4)接下來對年分進行修正,以將年份轉換為“19XX”的格式,如果是1970以前的年份,則將其加上100。?
(5)最后調用mktime()函數將當前時間與日期轉換為相對于1970-01-01?00:00:00的秒數值,并將其作為函數返回值返回。?
函數mktime()定義在include/linux/time.h頭文件中,它用來根據Gauss算法將以year/mon/day/hour/min/sec(如1980-12-31?23:59:59)格式表示的時間轉換為相對于1970-01-01?00:00:00這個UNIX時間基準以來的相對秒數。其源碼如下:?
static?inline?unsigned?long?
mktime?(unsigned?int?year,?unsigned?int?mon,?
unsigned?int?day,?unsigned?int?hour,?
unsigned?int?min,?unsigned?int?sec)?
{?
if?(0?>=?(int)?(mon?-=?2))?{?/*?1..12?->?11,12,1..10?*/?
mon?+=?12;?/*?Puts?Feb?last?since?it?has?leap?day?*/?
year?-=?1;?
}?
return?(((?
(unsigned?long)?(year/4?-?year/100?+?year/400?+?367*mon/12?+?day)?+?
year*365?-?719499?
)*24?+?hour?/*?now?have?hours?*/?
)*60?+?min?/*?now?have?minutes?*/?
)*60?+?sec;?/*?finally?seconds?*/?
}?
(3)set_rtc_mmss()函數?
該函數用來更新RTC中的時間,它僅有一個參數nowtime,是以秒數表示的當前時間,其源碼如下:?
static?int?set_rtc_mmss(unsigned?long?nowtime)?
{?
int?retval?=?0;?
int?real_seconds,?real_minutes,?cmos_minutes;?
unsigned?char?save_control,?save_freq_select;?
/*?gets?recalled?with?irq?locally?disabled?*/?
spin_lock(&rtc_lock);?
save_control?=?CMOS_READ(RTC_CONTROL);?/*?tell?the?clock?it's?being?set?*/?
CMOS_WRITE((save_control|RTC_SET),?RTC_CONTROL);?
save_freq_select?=?CMOS_READ(RTC_FREQ_SELECT);?/*?stop?and?reset?prescaler?*/?
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2),?RTC_FREQ_SELECT);?
cmos_minutes?=?CMOS_READ(RTC_MINUTES);?
if?(!(save_control?&?RTC_DM_BINARY)?||?RTC_ALWAYS_BCD)?
BCD_TO_BIN(cmos_minutes);?
/*?
*?since?we're?only?adjusting?minutes?and?seconds,?
*?don't?interfere?with?hour?overflow.?This?avoids?
*?messing?with?unknown?time?zones?but?requires?your?
*?RTC?not?to?be?off?by?more?than?15?minutes?
*/?
real_seconds?=?nowtime?%?60;?
real_minutes?=?nowtime?/?60;?
if?(((abs(real_minutes?-?cmos_minutes)?+?15)/30)?&?1)?
real_minutes?+=?30;?/*?correct?for?half?hour?time?zone?*/?
real_minutes?%=?60;?
if?(abs(real_minutes?-?cmos_minutes)?<?30)?{?
if?(!(save_control?&?RTC_DM_BINARY)?||?RTC_ALWAYS_BCD)?{?
BIN_TO_BCD(real_seconds);?
BIN_TO_BCD(real_minutes);?
}?
CMOS_WRITE(real_seconds,RTC_SECONDS);?
CMOS_WRITE(real_minutes,RTC_MINUTES);?
}?else?{?
printk(KERN_WARNING?
"set_rtc_mmss:?can't?update?from?%d?to?%d\n",?
cmos_minutes,?real_minutes);?
retval?=?-1;?
}?
/*?The?following?flags?have?to?be?released?exactly?in?this?order,?
*?otherwise?the?DS12887?(popular?MC146818A?clone?with?integrated?
*?battery?and?quartz)?will?not?reset?the?oscillator?and?will?not?
*?update?precisely?500?ms?later.?You?won't?find?this?mentioned?in?
*?the?Dallas?Semiconductor?data?sheets,?but?who?believes?data?
*?sheets?anyway?...?--?Markus?Kuhn?
*/?
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節)。
?dreamice 回復于:2008-11-06 17:56:21
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寄存器時才被使用,后面我們會詳細分析這個函數。
?dreamice 回復于:2008-11-06 17:56:41
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?
*?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-01?00: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)是最為迫切的,因此必須在時鐘中斷服務例程中完成。而其余的幾個要求可以稍緩,因此可以放在時鐘中斷的Bottom?Half中去執行。這樣,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()函數計算時間偏差之用。這一步的實質就是將CPU?TSC寄存器的值更新到內核對TSC的緩存變量last_tsc_low中。?
l?通過讀8254?PIT的通道0的計數器的當前值來計算時間延遲,為此:首先,對自旋鎖i8253_lock進行加鎖。自旋鎖i8253_lock的作用就是用來串行化對8254?PIT的讀寫訪問。其次,向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?accordingly?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存在Update?Cycle,因此最好在一秒時間間隔的中間位置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()函數更新當前進程的時間統計信息。注意,該函數的參數原型是“int?user_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]上。?
l?update_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軟件定時器更新時間間隔。?
l?do_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信號殺死該進程。?
l?do_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;?
}?
}?
l?do_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;?
}?
}
?dreamice 回復于:2008-11-06 17:57:10
7.5?時鐘中斷的Bottom?Half?
與時鐘中斷相關的Bottom?Half向兩主要有兩個:TIMER_BH和TQUEUE_BH。與TIMER_BH相對應的BH函數是timer_bh(),與TQUEUE_BH對應的函數是tqueue_bh()。它們均實現在kernel/timer.c文件中。?
7.5.1?TQUEUE_BH向量?
TQUEUE_BH的作用是用來運行tq_timer這個任務隊列中的任務。因此do_timer()函數僅僅在tq_timer任務隊列不為空的情況才激活TQUEUE_BH向量。函數tqueue_bh()的實現非常簡單,它只是簡單地調用run_task_queue()函數來運行任務隊列tq_timer。如下所示:?
void?tqueue_bh(void)?
{?
run_task_queue(&tq_timer);?
}?
任務對列tq_timer也是定義在kernel/timer.c文件中,如下所示:?
DECLARE_TASK_QUEUE(tq_timer);?
7.5.2?TIMER_BH向量?
TIMER_BH這個Bottom?Half向量是Linux內核時鐘中斷驅動的一個重要輔助部分。內核在每一次對時鐘中斷的服務快要結束時,都會無條件地激活一個TIMER_BH向量,以使得內核在稍后一段延遲后執行相應的BH函數——timer_bh()。該任務的源碼如下:?
void?timer_bh(void)?
{?
update_times();?
run_timer_list();?
}?
從上述源碼可以看出,內核在時鐘中斷驅動的底半部分主要有兩個任務:(1)調用update_times()函數來更新系統全局時間xtime;(2)調用run_timer_list()函數來執行定時器。關于定時器我們將在下一節討論。本節我們主要討論TIMER_BH的第一個任務——對內核時間xtime的更新。?
我們都知道,內核局部時間xtime是用來供用戶程序通過時間syscall來檢索或設置當前系統時間的,而內核代碼在大多數情況下都引用jiffies變量,而很少使用xtime(偶爾也會有引用xtime的情況,比如更新inode的時間標記)。因此,對于時鐘中斷服務程序timer_interrupt()而言,jiffies變量的更新是最緊迫的,而xtime的更新則可以延遲到中斷服務的底半部分來進行。?
由于Bottom?Half機制在執行時間具有某些不確定性,因此在timer_bh()函數得到真正執行之前,期間可能又會有幾次時鐘中斷發生。這樣就會造成時鐘滴答的丟失現象。為了處理這種情況,Linux內核使用了一個輔助全局變量wall_jiffies,來表示上一次更新xtime時的jiffies值。其定義如下(kernel/timer.c):?
/*?jiffies?at?the?most?recent?update?of?wall?time?*/?
unsigned?long?wall_jiffies;?
而timer_bh()函數真正執行時的jiffies值與wall_jiffies的差就是在timer_bh()真正執行之前所發生的時鐘中斷次數。?
函數update_times()的源碼如下(kernel/timer.c):?
static?inline?void?update_times(void)?
{?
unsigned?long?ticks;?
/*?
*?update_times()?is?run?from?the?raw?timer_bh?handler?so?we?
*?just?know?that?the?irqs?are?locally?enabled?and?so?we?don't?
*?need?to?save/restore?the?flags?of?the?local?CPU?here.?-arca?
*/?
write_lock_irq(&xtime_lock);?
ticks?=?jiffies?-?wall_jiffies;?
if?(ticks)?{?
wall_jiffies?+=?ticks;?
update_wall_time(ticks);?
}?
write_unlock_irq(&xtime_lock);?
calc_load(ticks);?
}?
(1)首先,根據jiffies和wall_jiffies的差值計算在此之前一共發生了幾次時鐘滴答,并將這個值保存到局部變量ticks中。并在ticks值大于0的情況下(ticks大于等于1,一般情況下為1):①更新wall_jiffies為jiffies變量的當前值(wall_jiffies+=ticks等價于wall_jiffies=jiffies)。②以參數ticks調用update_wall_time()函數去真正地更新全局時間xtime。?
(2)調用calc_load()函數去計算系統負載情況。這里我們不去深究它。?
函數update_wall_time()函數根據參數ticks所指定的時鐘滴答次數相應地更新內核全局時間變量xtime。其源碼如下(kernel/timer.c):?
/*?
*?Using?a?loop?looks?inefficient,?but?"ticks"?is?
*?usually?just?one?(we?shouldn't?be?losing?ticks,?
*?we're?doing?this?this?way?mainly?for?interrupt?
*?latency?reasons,?not?because?we?think?we'll?
*?have?lots?of?lost?timer?ticks?
*/?
static?void?update_wall_time(unsigned?long?ticks)?
{?
do?{?
ticks--;?
update_wall_time_one_tick();?
}?while?(ticks);?
if?(xtime.tv_usec?>=?1000000)?{?
xtime.tv_usec?-=?1000000;?
xtime.tv_sec++;?
second_overflow();?
}?
}?
對該函數的注釋如下:?
(1)首先,用一個do{}循環來根據參數ticks的值一次一次調用update_wall_time_one_tick()函數來為一次時鐘滴答更新xtime中的tv_usec成員。?
(2)根據需要調整xtime中的秒數成員tv_usec和微秒數成員tv_usec。如果微秒數成員tv_usec的值超過106,則說明已經過了一秒鐘。因此將tv_usec的值減去1000000,并將秒數成員tv_sec的值加1,然后調用second_overflow()函數來處理微秒數成員溢出的情況。?
函數update_wall_time_one_tick()用來更新一次時鐘滴答對系統全局時間xtime的影響。由于tick全局變量表示了一次時鐘滴答的時間間隔長度(以us為單位),因此該函數的實現中最核心的代碼就是將xtime的tv_usec成員增加tick微秒。這里我們不去關心函數實現中與NTP(Network?Time?Protocol)和系統調用adjtimex()的相關部分。其源碼如下(kernel/timer.c):?
/*?in?the?NTP?reference?this?is?called?"hardclock()"?*/?
static?void?update_wall_time_one_tick(void)?
{?
if?(?(time_adjust_step?=?time_adjust)?!=?0?)?{?
/*?We?are?doing?an?adjtime?thing.?
*?
*?Prepare?time_adjust_step?to?be?within?bounds.?
*?Note?that?a?positive?time_adjust?means?we?want?the?clock?
*?to?run?faster.?
*?
*?Limit?the?amount?of?the?step?to?be?in?the?range?
*?-tickadj?..?+tickadj?
*/?
if?(time_adjust?>?tickadj)?
time_adjust_step?=?tickadj;?
else?if?(time_adjust?<?-tickadj)?
time_adjust_step?=?-tickadj;?
/*?Reduce?by?this?step?the?amount?of?time?left?*/?
time_adjust?-=?time_adjust_step;?
}?
xtime.tv_usec?+=?tick?+?time_adjust_step;?
/*?
*?Advance?the?phase,?once?it?gets?to?one?microsecond,?then?
*?advance?the?tick?more.?
*/?
time_phase?+=?time_adj;?
if?(time_phase?<=?-FINEUSEC)?{?
long?ltemp?=?-time_phase?>>?SHIFT_SCALE;?
time_phase?+=?ltemp?<<?SHIFT_SCALE;?
xtime.tv_usec?-=?ltemp;?
}?
else?if?(time_phase?>=?FINEUSEC)?{?
long?ltemp?=?time_phase?>>?SHIFT_SCALE;?
time_phase?-=?ltemp?<<?SHIFT_SCALE;?
xtime.tv_usec?+=?ltemp;?
}?
}
?dreamice 回復于:2008-11-06 17:57:33
7.6?內核定時器機制?
Linux內核2.4版中去掉了老版本內核中的靜態定時器機制,而只留下動態定時器。相應地在timer_bh()函數中也不再通過run_old_timers()函數來運行老式的靜態定時器。動態定時器與靜態定時器這二個概念是相對于Linux內核定時器機制的可擴展功能而言的,動態定時器是指內核的定時器隊列是可以動態變化的,然而就定時器本身而言,二者并無本質的區別??紤]到靜態定時器機制的能力有限,因此Linux內核2.4版中完全去掉了以前的靜態定時器機制。?
7.6.1?Linux內核對定時器的描述?
Linux在include/linux/timer.h頭文件中定義了數據結構timer_list來描述一個內核定時器:?
struct?timer_list?{?
struct?list_head?list;?
unsigned?long?expires;?
unsigned?long?data;?
void?(*function)(unsigned?long);?
};?
各數據成員的含義如下:?
(1)雙向鏈表元素list:用來將多個定時器連接成一條雙向循環隊列。?
(2)expires:指定定時器到期的時間,這個時間被表示成自系統啟動以來的時鐘滴答計數(也即時鐘節拍數)。當一個定時器的expires值小于或等于jiffies變量時,我們就說這個定時器已經超時或到期了。在初始化一個定時器后,通常把它的expires域設置成當前expires變量的當前值加上某個時間間隔值(以時鐘滴答次數計)。?
(3)函數指針function:指向一個可執行函數。當定時器到期時,內核就執行function所指定的函數。而data域則被內核用作function函數的調用參數。?
內核函數init_timer()用來初始化一個定時器。實際上,這個初始化函數僅僅將結構中的list成員初始化為空。如下所示(include/linux/timer.h):?
static?inline?void?init_timer(struct?timer_list?*?timer)?
{?
timer->list.next?=?timer->list.prev?=?NULL;?
}?
由于定時器通常被連接在一個雙向循環隊列中等待執行(此時我們說定時器處于pending狀態)。因此函數time_pending()就可以用list成員是否為空來判斷一個定時器是否處于pending狀態。如下所示(include/linux/timer.h):?
static?inline?int?timer_pending?(const?struct?timer_list?*?timer)?
{?
return?timer->list.next?!=?NULL;?
}?
l?時間比較操作?
在定時器應用中經常需要比較兩個時間值,以確定timer是否超時,所以Linux內核在timer.h頭文件中定義了4個時間關系比較操作宏。這里我們說時刻a在時刻b之后,就意味著時間值a≥b。Linux強烈推薦用戶使用它所定義的下列4個時間比較操作宏(include/linux/timer.h):?
#define?time_after(a,b)?((long)(b)?-?(long)(a)?<?0)?
#define?time_before(a,b)?time_after(b,a)?
#define?time_after_eq(a,b)?((long)(a)?-?(long)(b)?>=?0)?
#define?time_before_eq(a,b)?time_after_eq(b,a)?
7.6.2?動態內核定時器機制的原理?
Linux是怎樣為其內核定時器機制提供動態擴展能力的呢?其關鍵就在于“定時器向量”的概念。所謂“定時器向量”就是指這樣一條雙向循環定時器隊列(對列中的每一個元素都是一個timer_list結構):對列中的所有定時器都在同一個時刻到期,也即對列中的每一個timer_list結構都具有相同的expires值。顯然,可以用一個timer_list結構類型的指針來表示一個定時器向量。?
顯然,定時器expires成員的值與jiffies變量的差值決定了一個定時器將在多長時間后到期。在32位系統中,這個時間差值的最大值應該是0xffffffff。因此如果是基于“定時器向量”基本定義,內核將至少要維護0xffffffff個timer_list結構類型的指針,這顯然是不現實的。?
另一方面,從內核本身這個角度看,它所關心的定時器顯然不是那些已經過期而被執行過的定時器(這些定時器完全可以被丟棄),也不是那些要經過很長時間才會到期的定時器,而是那些當前已經到期或者馬上就要到期的定時器(注意!時間間隔是以滴答次數為計數單位的)。?
基于上述考慮,并假定一個定時器要經過interval個時鐘滴答后才到期(interval=expires-jiffies),則Linux采用了下列思想來實現其動態內核定時器機制:對于那些0≤interval≤255的定時器,Linux嚴格按照定時器向量的基本語義來組織這些定時器,也即Linux內核最關心那些在接下來的255個時鐘節拍內就要到期的定時器,因此將它們按照各自不同的expires值組織成256個定時器向量。而對于那些256≤interval≤0xffffffff的定時器,由于他們離到期還有一段時間,因此內核并不關心他們,而是將它們以一種擴展的定時器向量語義(或稱為“松散的定時器向量語義”)進行組織。所謂“松散的定時器向量語義”就是指:各定時器的expires值可以互不相同的一個定時器隊列。?
具體的組織方案可以分為兩大部分:?
(1)對于內核最關心的、interval值在[0,255]之間的前256個定時器向量,內核是這樣組織它們的:這256個定時器向量被組織在一起組成一個定時器向量數組,并作為數據結構timer_vec_root的一部分,該數據結構定義在kernel/timer.c文件中,如下述代碼段所示:?
/*?
*?Event?timer?code?
*/?
#define?TVN_BITS?6?
#define?TVR_BITS?8?
#define?TVN_SIZE?(1?<<?TVN_BITS)?
#define?TVR_SIZE?(1?<<?TVR_BITS)?
#define?TVN_MASK?(TVN_SIZE?-?1)?
#define?TVR_MASK?(TVR_SIZE?-?1)?
struct?timer_vec?{?
int?index;?
struct?list_head?vec[TVN_SIZE];?
};?
struct?timer_vec_root?{?
int?index;?
struct?list_head?vec[TVR_SIZE];?
};?
static?struct?timer_vec?tv5;?
static?struct?timer_vec?tv4;?
static?struct?timer_vec?tv3;?
static?struct?timer_vec?tv2;?
static?struct?timer_vec_root?tv1;?
static?struct?timer_vec?*?const?tvecs[]?=?{?
(struct?timer_vec?*)&tv1,?&tv2,?&tv3,?&tv4,?&tv5?
};?
#define?NOOF_TVECS?(sizeof(tvecs)?/?sizeof(tvecs[0]))?
基于數據結構timer_vec_root,Linux定義了一個全局變量tv1,以表示內核所關心的前256個定時器向量。這樣內核在處理是否有到期定時器時,它就只從定時器向量數組tv1.vec[256]中的某個定時器向量內進行掃描。而tv1的index字段則指定當前正在掃描定時器向量數組tv1.vec[256]中的哪一個定時器向量,也即該數組的索引,其初值為0,最大值為255(以256為模)。每個時鐘節拍時index字段都會加1。顯然,index字段所指定的定時器向量tv1.vec[index]中包含了當前時鐘節拍內已經到期的所有動態定時器。而定時器向量tv1.vec[index+k]則包含了接下來第k個時鐘節拍時刻將到期的所有動態定時器。當index值又重新變為0時,就意味著內核已經掃描了tv1變量中的所有256個定時器向量。在這種情況下就必須將那些以松散定時器向量語義來組織的定時器向量補充到tv1中來。?
(2)而對于內核不關心的、interval值在[0xff,0xffffffff]之間的定時器,它們的到期緊迫程度也隨其interval值的不同而不同。顯然interval值越小,定時器緊迫程度也越高。因此在將它們以松散定時器向量進行組織時也應該區別對待。通常,定時器的interval值越小,它所處的定時器向量的松散度也就越低(也即向量中的各定時器的expires值相差越小);而interval值越大,它所處的定時器向量的松散度也就越大(也即向量中的各定時器的expires值相差越大)。?
內核規定,對于那些滿足條件:0x100≤interval≤0x3fff的定時器,只要表達式(interval>>8)具有相同值的定時器都將被組織在同一個松散定時器向量中。因此,為組織所有滿足條件0x100≤interval≤0x3fff的定時器,就需要26=64個松散定時器向量。同樣地,為方便起見,這64個松散定時器向量也放在一起形成數組,并作為數據結構timer_vec的一部分?;跀祿Y構timer_vec,Linux定義了全局變量tv2,來表示這64條松散定時器向量。如上述代碼段所示。?
對于那些滿足條件0x4000≤interval≤0xfffff的定時器,只要表達式(interval>>8+6)的值相同的定時器都將被放在同一個松散定時器向量中。同樣,要組織所有滿足條件0x4000≤interval≤0xfffff的定時器,也需要26=64個松散定時器向量。類似地,這64個松散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv3全局變量來表示這64個松散定時器向量。?
對于那些滿足條件0x100000≤interval≤0x3ffffff的定時器,只要表達式(interval>>8+6+6)的值相同的定時器都將被放在同一個松散定時器向量中。同樣,要組織所有滿足條件0x100000≤interval≤0x3ffffff的定時器,也需要26=64個松散定時器向量。類似地,這64個松散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv4全局變量來表示這64個松散定時器向量。?
對于那些滿足條件0x4000000≤interval≤0xffffffff的定時器,只要表達式(interval>>8+6+6+6)的值相同的定時器都將被放在同一個松散定時器向量中。同樣,要組織所有滿足條件0x4000000≤interval≤0xffffffff的定時器,也需要26=64個松散定時器向量。類似地,這64個松散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv5全局變量來表示這64個松散定時器向量。?
最后,為了引用方便,Linux定義了一個指針數組tvecs[],來分別指向tv1、tv2、…、tv5結構變量。如上述代碼所示。?
整個內核定時器機制的總體結構如下圖7-8所示:?
7.6.3?內核動態定時器機制的實現?
在內核動態定時器機制的實現中,有三個操作時非常重要的:(1)將一個定時器插入到它應該所處的定時器向量中。(2)定時器的遷移,也即將一個定時器從它原來所處的定時器向量遷移到另一個定時器向量中。(3)掃描并執行當前已經到期的定時器。?
7.6.3.1?動態定時器機制的初始化?
函數init_timervecs()實現對動態定時器機制的初始化。該函數僅被sched_init()初始化例程所調用。動態定時器機制初始化過程的主要任務就是將tv1、tv2、…、tv5這5個結構變量中的定時器向量指針數組vec[]初始化為NULL。如下所示(kernel/timer.c):?
void?init_timervecs?(void)?
{?
int?i;?
for?(i?=?0;?i?<?TVN_SIZE;?i++)?{?
INIT_LIST_HEAD(tv5.vec?+?i);?
INIT_LIST_HEAD(tv4.vec?+?i);?
INIT_LIST_HEAD(tv3.vec?+?i);?
INIT_LIST_HEAD(tv2.vec?+?i);?
}?
for?(i?=?0;?i?<?TVR_SIZE;?i++)?
INIT_LIST_HEAD(tv1.vec?+?i);?
}?
上述函數中的宏TVN_SIZE是指timer_vec結構類型中的定時器向量指針數組vec[]的大小,值為64。宏TVR_SIZE是指timer_vec_root結構類型中的定時器向量數組vec[]的大小,值為256。?
7.6.3.2?動態定時器的時鐘滴答基準timer_jiffies?
由于動態定時器是在時鐘中斷的Bottom?Half中被執行的,而從TIMER_BH向量被激活到其timer_bh()函數真正執行這段時間內可能會有幾次時鐘中斷發生。因此內核必須記住上一次運行定時器機制是什么時候,也即內核必須保存上一次運行定時器機制時的jiffies值。為此,Linux在kernel/timer.c文件中定義了全局變量timer_jiffies來表示上一次運行定時器機制時的jiffies值。該變量的定義如下所示:?
static?unsigned?long?timer_jiffies;?
7.6.3.3?對內核動態定時器鏈表的保護?
由于內核動態定時器鏈表是一種系統全局共享資源,為了實現對它的互斥訪問,Linux定義了專門的自旋鎖timerlist_lock來保護。任何想要訪問動態定時器鏈表的代碼段都首先必須先持有該自旋鎖,并且在訪問結束后釋放該自旋鎖。其定義如下(kernel/timer.c):?
/*?Initialize?both?explicitly?-?let's?try?to?have?them?in?the?same?cache?line?*/?
spinlock_t?timerlist_lock?=?SPIN_LOCK_UNLOCKED;?
7.6.3.4?將一個定時器插入到鏈表中?
函數add_timer()用來將參數timer指針所指向的定時器插入到一個合適的定時器鏈表中。它首先調用timer_pending()函數判斷所指定的定時器是否已經位于在某個定時器向量中等待執行。如果是,則不進行任何操作,只是打印一條內核告警信息就返回了;如果不是,則調用internal_add_timer()函數完成實際的插入操作。其源碼如下(kernel/timer.c):?
void?add_timer(struct?timer_list?*timer)?
{?
unsigned?long?flags;?
spin_lock_irqsave(&timerlist_lock,?flags);?
if?(timer_pending(timer))?
goto?bug;?
internal_add_timer(timer);?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
return;?
bug:?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
printk("bug:?kernel?timer?added?twice?at?%p.\n",?
__builtin_return_address(0));?
}?
函數internal_add_timer()用于將一個不處于任何定時器向量中的定時器插入到它應該所處的定時器向量中去(根據定時器的expires值來決定)。如下所示(kernel/timer.c):?
static?inline?void?internal_add_timer(struct?timer_list?*timer)?
{?
/*?
*?must?be?cli-ed?when?calling?this?
*/?
unsigned?long?expires?=?timer->expires;?
unsigned?long?idx?=?expires?-?timer_jiffies;?
struct?list_head?*?vec;?
if?(idx?<?TVR_SIZE)?{?
int?i?=?expires?&?TVR_MASK;?
vec?=?tv1.vec?+?i;?
}?else?if?(idx?<?1?<<?(TVR_BITS?+?TVN_BITS))?{?
int?i?=?(expires?>>?TVR_BITS)?&?TVN_MASK;?
vec?=?tv2.vec?+?i;?
}?else?if?(idx?<?1?<<?(TVR_BITS?+?2?*?TVN_BITS))?{?
int?i?=?(expires?>>?(TVR_BITS?+?TVN_BITS))?&?TVN_MASK;?
vec?=?tv3.vec?+?i;?
}?else?if?(idx?<?1?<<?(TVR_BITS?+?3?*?TVN_BITS))?{?
int?i?=?(expires?>>?(TVR_BITS?+?2?*?TVN_BITS))?&?TVN_MASK;?
vec?=?tv4.vec?+?i;?
}?else?if?((signed?long)?idx?<?0)?{?
/*?can?happen?if?you?add?a?timer?with?expires?==?jiffies,?
*?or?you?set?a?timer?to?go?off?in?the?past?
*/?
vec?=?tv1.vec?+?tv1.index;?
}?else?if?(idx?<=?0xffffffffUL)?{?
int?i?=?(expires?>>?(TVR_BITS?+?3?*?TVN_BITS))?&?TVN_MASK;?
vec?=?tv5.vec?+?i;?
}?else?{?
/*?Can?only?get?here?on?architectures?with?64-bit?jiffies?*/?
INIT_LIST_HEAD(&timer->list);?
return;?
}?
/*?
*?Timers?are?FIFO!?
*/?
list_add(&timer->list,?vec->prev);?
}?
對該函數的注釋如下:?
(1)首先,計算定時器的expires值與timer_jiffies的插值(注意!這里應該使用動態定時器自己的時間基準),這個差值就表示這個定時器相對于上一次運行定時器機制的那個時刻還需要多長時間間隔才到期。局部變量idx保存這個差值。?
(2)根據idx的值確定這個定時器應被插入到哪一個定時器向量中。其具體的確定方法我們在7.6.2節已經說過了,這里不再詳述。最后,定時器向量的頭部指針vec表示這個定時器應該所處的定時器向量鏈表頭部。?
(3)最后,調用list_add()函數將定時器插入到vec指針所指向的定時器隊列的尾部。?
7.6.3.5?修改一個定時器的expires值?
當一個定時器已經被插入到內核動態定時器鏈表中后,我們還可以修改該定時器的expires值。函數mod_timer()實現這一點。如下所示(kernel/timer.c):?
int?mod_timer(struct?timer_list?*timer,?unsigned?long?expires)?
{?
int?ret;?
unsigned?long?flags;?
spin_lock_irqsave(&timerlist_lock,?flags);?
timer->expires?=?expires;?
ret?=?detach_timer(timer);?
internal_add_timer(timer);?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
return?ret;?
}?
該函數首先根據參數expires值更新定時器的expires成員。然后調用detach_timer()函數將該定時器從它原來所屬的鏈表中刪除。最后調用internal_add_timer()函數將該定時器根據它新的expires值重新插入到相應的鏈表中。?
函數detach_timer()首先調用timer_pending()來判斷指定的定時器是否已經處于某個鏈表中,如果定時器原來就不處于任何鏈表中,則detach_timer()函數什么也不做,直接返回0值,表示失敗。否則,就調用list_del()函數將定時器從它原來所處的鏈表中摘除。如下所示(kernel/timer.c):?
static?inline?int?detach_timer?(struct?timer_list?*timer)?
{?
if?(!timer_pending(timer))?
return?0;?
list_del(&timer->list);?
return?1;?
}?
7.6.3.6?刪除一個定時器?
函數del_timer()用來將一個定時器從相應的內核定時器隊列中刪除。該函數實際上是對detach_timer()函數的高層封裝。如下所示(kernel/timer.c):?
int?del_timer(struct?timer_list?*?timer)?
{?
int?ret;?
unsigned?long?flags;?
spin_lock_irqsave(&timerlist_lock,?flags);?
ret?=?detach_timer(timer);?
timer->list.next?=?timer->list.prev?=?NULL;?
spin_unlock_irqrestore(&timerlist_lock,?flags);?
return?ret;?
}?
7.6.3.7?定時器遷移操作?
由于一個定時器的interval值會隨著時間的不斷流逝(即jiffies值的不斷增大)而不斷變小,因此那些原本到期緊迫程度較低的定時器會隨著jiffies值的不斷增大而成為既將馬上到期的定時器。比如定時器向量tv2.vec[0]中的定時器在經過256個時鐘滴答后會成為未來256個時鐘滴答內會到期的定時器。因此,定時器在內核動態定時器鏈表中的位置也應相應地隨著改變。改變的規則是:當tv1.index重新變為0時(意味著tv1中的256個定時器向量都已被內核掃描一遍了,從而使tv1中的256個定時器向量變為空),則用tv2.vec[index]定時器向量中的定時器去填充tv1,同時使tv2.index加1(它以64為模)。當tv2.index重新變為0(意味著tv2中的64個定時器向量都已經被全部填充到tv1中去了,從而使得tv2變為空),則用tv3.vec[index]定時器向量中的定時器去填充tv2。如此一直類推下去,直到tv5。?
函數cascade_timers()完成這種定時器遷移操作,該函數只有一個timer_vec結構類型指針的參數tv。這個函數將把定時器向量tv->vec[tv->index]中的所有定時器重新填充到上一層定時器向量中去。如下所示(kernel/timer.c):?
static?inline?void?cascade_timers(struct?timer_vec?*tv)?
{?
/*?cascade?all?the?timers?from?tv?up?one?level?*/?
struct?list_head?*head,?*curr,?*next;?
head?=?tv->vec?+?tv->index;?
curr?=?head->next;?
/*?
*?We?are?removing?_all_?timers?from?the?list,?so?we?don't?have?to?
*?detach?them?individually,?just?clear?the?list?afterwards.?
*/?
while?(curr?!=?head)?{?
struct?timer_list?*tmp;?
tmp?=?list_entry(curr,?struct?timer_list,?list);?
next?=?curr->next;?
list_del(curr);?//?not?needed?
internal_add_timer(tmp);?
curr?=?next;?
}?
INIT_LIST_HEAD(head);?
tv->index?=?(tv->index?+?1)?&?TVN_MASK;?
}?
對該函數的注釋如下:?
(1)首先,用指針head指向定時器頭部向量頭部的list_head結構。指針curr指向定時器向量中的第一個定時器。?
(2)然后,用一個while{}循環來遍歷定時器向量tv->vec[tv->index]。由于定時器向量是一個雙向循環隊列,因此循環的終止條件是curr=head。對于每一個被掃描的定時器,循環體都先調用list_del()函數將當前定時器從鏈表中摘除,然后調用internal_add_timer()函數重新確定該定時器應該被放到哪個定時器向量中去。?
(3)當從while{}循環退出后,定時器向量tv->vec[tv->index]中所有的定時器都已被遷移到其它地方(到它們該呆的地方:-),因此它本身就成為一個空隊列。這里我們顯示地調用INIT_LIST_HEAD()宏來將定時器向量的表頭結構初始化為空。?
(4)最后,將tv->index值加1,當然它是以64為模。?
7.6.4.8?掃描并執行當前已經到期的定時器?
函數run_timer_list()完成這個功能。如前所述,該函數是被timer_bh()函數所調用的,因此內核定時器是在時鐘中斷的Bottom?Half中被執行的。記住這一點非常重要。全局變量timer_jiffies表示了內核上一次執行run_timer_list()函數的時間,因此jiffies與timer_jiffies的差值就表示了自從上一次處理定時器以來,期間一共發生了多少次時鐘中斷,顯然run_timer_list()函數必須為期間所發生的每一次時鐘中斷補上定時器服務。該函數的源碼如下(kernel/timer.c):?
static?inline?void?run_timer_list(void)?
{?
spin_lock_irq(&timerlist_lock);?
while?((long)(jiffies?-?timer_jiffies)?>=?0)?{?
struct?list_head?*head,?*curr;?
if?(!tv1.index)?{?
int?n?=?1;?
do?{?
cascade_timers(tvecs[n]);?
}?while?(tvecs[n]->index?==?1?&&?++n?<?NOOF_TVECS);?
}?
repeat:?
head?=?tv1.vec?+?tv1.index;?
curr?=?head->next;?
if?(curr?!=?head)?{?
struct?timer_list?*timer;?
void?(*fn)(unsigned?long);?
unsigned?long?data;?
timer?=?list_entry(curr,?struct?timer_list,?list);?
fn?=?timer->function;?
data=?timer->data;?
detach_timer(timer);?
timer->list.next?=?timer->list.prev?=?NULL;?
timer_enter(timer);?
spin_unlock_irq(&timerlist_lock);?
fn(data);?
spin_lock_irq(&timerlist_lock);?
timer_exit();?
goto?repeat;?
}?
++timer_jiffies;?
tv1.index?=?(tv1.index?+?1)?&?TVR_MASK;?
}?
spin_unlock_irq(&timerlist_lock);?
}?
函數run_timer_list()的執行過程主要就是用一個大while{}循環來為時鐘中斷執行定時器服務,每一次循環服務一次時鐘中斷。因此一共要執行(jiffies-timer_jiffies+1)次循環。循環體所執行的服務步驟如下:?
(1)首先,判斷tv1.index是否為0,如果為0則需要從tv2中補充定時器到tv1中來。但tv2也可能為空而需要從tv3中補充定時器,因此用一個do{}while循環來調用cascade_timer()函數來依次視需要從tv2中補充tv1,從tv3中補充tv2、…、從tv5中補充tv4。顯然如果tvi.index=0(2≤i≤5),則對于tvi執行cascade_timers()函數后,tvi.index肯定為1。反過來講,如果對tvi執行過cascade_timers()函數后tvi.index不等于1,那么可以肯定在未對tvi執行cascade_timers()函數之前,tvi.index值肯定不為0,因此這時tvi不需要從tv(i+1)中補充定時器,這時就可以終止do{}while循環。?
(2)接下來,就要執行定時器向量tv1.vec[tv1.index]中的所有到期定時器。因此這里用一個goto?repeat循環從頭到尾依次掃描整個定時器對列。由于在執行定時器的關聯函數時并不需要關CPU中斷,所以在用detach_timer()函數將當前定時器從對列中摘除后,就可以調用spin_unlock_irq()函數進行解鎖和開中斷,然后在執行完當前定時器的關聯函數后重新用spin_lock_irq()函數加鎖和關中斷。?
(3)當執行完定時器向量tv1.vec[tv1.index]中的所有到期定時器后,tv1.vec[tv1.index]應該是個空隊列。至此這一次定時器服務也就宣告結束。?
(4)最后,將timer_jiffies值加1,將tv1.index值加1,當然它的模是256。然后,回到while循環開始下一次定時器服務。
?dreamice 回復于:2008-11-06 17:57:51
7.7?進程間隔定時器itimer?
所謂“間隔定時器(Interval?Timer,簡稱itimer)就是指定時器采用“間隔”值(interval)來作為計時方式,當定時器啟動后,間隔值interval將不斷減小。當interval值減到0時,我們就說該間隔定時器到期。與上一節所說的內核動態定時器相比,二者最大的區別在于定時器的計時方式不同。內核定時器是通過它的到期時刻expires值來計時的,當全局變量jiffies值大于或等于內核動態定時器的expires值時,我們說內核內核定時器到期。而間隔定時器則實際上是通過一個不斷減小的計數器來計時的。雖然這兩種定時器并不相同,但卻也是相互聯系的。假如我們每個時鐘節拍都使間隔定時器的間隔計數器減1,那么在這種情形下間隔定時器實際上就是內核動態定時器(下面我們會看到進程的真實間隔定時器就是這樣通過內核定時器來實現的)。?
間隔定時器主要被應用在用戶進程上。每個Linux進程都有三個相互關聯的間隔定時器。其各自的間隔計數器都定義在進程的task_struct結構中,如下所示(include/linux/sched.h):?
struct?task_struct{?
……?
unsigned?long?it_real_value,?it_prof_value,?it_virt_value;?
unsigned?long?it_real_incr,?it_prof_incr,?it_virt_incr;?
struct?timer_list?real_timer;?
……?
}?
(1)真實間隔定時器(ITIMER_REAL):這種間隔定時器在啟動后,不管進程是否運行,每個時鐘滴答都將其間隔計數器減1。當減到0值時,內核向進程發送SIGALRM信號。結構類型task_struct中的成員it_real_incr則表示真實間隔定時器的間隔計數器的初始值,而成員it_real_value則表示真實間隔定時器的間隔計數器的當前值。由于這種間隔定時器本質上與上一節的內核定時器時一樣的,因此Linux實際上是通過real_timer這個內嵌在task_struct結構中的內核動態定時器來實現真實間隔定時器ITIMER_REAL的。?
(2)虛擬間隔定時器ITIMER_VIRT:也稱為進程的用戶態間隔定時器。結構類型task_struct中成員it_virt_incr和it_virt_value分別表示虛擬間隔定時器的間隔計數器的初始值和當前值,二者均以時鐘滴答次數位計數單位。當虛擬間隔定時器啟動后,只有當進程在用戶態下運行時,一次時鐘滴答才能使間隔計數器當前值it_virt_value減1。當減到0值時,內核向進程發送SIGVTALRM信號(虛擬鬧鐘信號),并將it_virt_value重置為初值it_virt_incr。具體請見7.4.3節中的do_it_virt()函數的實現。?
(3)PROF間隔定時器ITIMER_PROF:進程的task_struct結構中的it_prof_value和it_prof_incr成員分別表示PROF間隔定時器的間隔計數器的當前值和初始值(均以時鐘滴答為單位)。當一個進程的PROF間隔定時器啟動后,則只要該進程處于運行中,而不管是在用戶態或核心態下執行,每個時鐘滴答都使間隔計數器it_prof_value值減1。當減到0值時,內核向進程發送SIGPROF信號,并將it_prof_value重置為初值it_prof_incr。具體請見7.4.3節的do_it_prof()函數。?
Linux在include/linux/time.h頭文件中為上述三種進程間隔定時器定義了索引標識,如下所示:?
#define?ITIMER_REAL?0?
#define?ITIMER_VIRTUAL?1?
#define?ITIMER_PROF?2?
7.7.1?數據結構itimerval?
雖然,在內核中間隔定時器的間隔計數器是以時鐘滴答次數為單位,但是讓用戶以時鐘滴答為單位來指定間隔定時器的間隔計數器的初值顯然是不太方便的,因為用戶習慣的時間單位是秒、毫秒或微秒等。所以Linux定義了數據結構itimerval來讓用戶以秒或微秒為單位指定間隔定時器的時間間隔值。其定義如下(include/linux/time.h):?
struct?itimerval?{?
struct?timeval?it_interval;?/*?timer?interval?*/?
struct?timeval?it_value;?/*?current?value?*/?
};?
其中,it_interval成員表示間隔計數器的初始值,而it_value成員表示間隔計數器的當前值。這兩個成員都是timeval結構類型的變量,因此其精度可以達到微秒級。?
l?timeval與jiffies之間的相互轉換?
由于間隔定時器的間隔計數器的內部表示方式與外部表現方式互不相同,因此有必要實現以微秒為單位的timeval結構和為時鐘滴答次數單位的jiffies之間的相互轉換。為此,Linux在kernel/itimer.c中實現了兩個函數實現二者的互相轉換——tvtojiffies()函數和jiffiestotv()函數。它們的源碼如下:?
static?unsigned?long?tvtojiffies(struct?timeval?*value)?
{?
unsigned?long?sec?=?(unsigned)?value->tv_sec;?
unsigned?long?usec?=?(unsigned)?value->tv_usec;?
if?(sec?>?(ULONG_MAX?/?HZ))?
return?ULONG_MAX;?
usec?+=?1000000?/?HZ?-?1;?
usec?/=?1000000?/?HZ;?
return?HZ*sec+usec;?
}?
static?void?jiffiestotv(unsigned?long?jiffies,?struct?timeval?*value)?
{?
value->tv_usec?=?(jiffies?%?HZ)?*?(1000000?/?HZ);?
value->tv_sec?=?jiffies?/?HZ;?
}?
7.7.2?真實間隔定時器ITIMER_REAL的底層運行機制?
間隔定時器ITIMER_VIRT和ITIMER_PROF的底層運行機制是分別通過函數do_it_virt()函數和do_it_prof()函數來實現的,這里就不再重述(可以參見7.4.3節)。?
由于間隔定時器ITIMER_REAL本質上與內核動態定時器并無區別。因此內核實際上是通過內核動態定時器來實現進程的ITIMER_REAL間隔定時器的。為此,task_struct結構中專門設立一個timer_list結構類型的成員變量real_timer。動態定時器real_timer的函數指針function總是被task_struct結構的初始化宏INIT_TASK設置為指向函數it_real_fn()。如下所示(include/linux/sched.h):?
#define?INIT_TASK(tsk)?\?
……?
real_timer:?{?
function:?it_real_fn?\?
}?\?
……?
}?
而real_timer鏈表元素list和data成員總是被進程創建時分別初始化為空和進程task_struct結構的地址,如下所示(kernel/fork.c):?
int?do_fork(……)?
{?
……?
p->it_real_value?=?p->it_virt_value?=?p->it_prof_value?=?0;?
p->it_real_incr?=?p->it_virt_incr?=?p->it_prof_incr?=?0;?
init_timer(&p->real_timer);?
p->real_timer.data?=?(unsigned?long)p;?
……?
}?
當用戶通過setitimer()系統調用來設置進程的ITIMER_REAL間隔定時器時,it_real_incr被設置成非零值,于是該系統調用相應地設置好real_timer.expires值,然后進程的real_timer定時器就被加入到內核動態定時器鏈表中,這樣該進程的ITIMER_REAL間隔定時器就被啟動了。當real_timer定時器到期時,它的關聯函數it_real_fn()將被執行。注意!所有進程的real_timer定時器的function函數指針都指向it_real_fn()這同一個函數,因此it_real_fn()函數必須通過其參數來識別是哪一個進程,為此它將unsigned?long類型的參數p解釋為進程task_struct結構的地址。該函數的源碼如下(kernel/itimer.c):?
void?it_real_fn(unsigned?long?__data)?
{?
struct?task_struct?*?p?=?(struct?task_struct?*)?__data;?
unsigned?long?interval;?
send_sig(SIGALRM,?p,?1);?
interval?=?p->it_real_incr;?
if?(interval)?{?
if?(interval?>?(unsigned?long)?LONG_MAX)?
interval?=?LONG_MAX;?
p->real_timer.expires?=?jiffies?+?interval;?
add_timer(&p->real_timer);?
}?
}?
函數it_real_fn()的執行過程大致如下:?
(1)首先將參數p通過強制類型轉換解釋為進程的task_struct結構類型的指針。?
(2)向進程發送SIGALRM信號。?
(3)在進程的it_real_incr非0的情況下繼續啟動real_timer定時器。首先,計算real_timer定時器的expires值為(jiffies+it_real_incr)。然后,調用add_timer()函數將real_timer加入到內核動態定時器鏈表中。?
7.7.3?itimer定時器的系統調用?
與itimer定時器相關的syscall有兩個:getitimer()和setitimer()。其中,getitimer()用于查詢調用進程的三個間隔定時器的信息,而setitimer()則用來設置調用進程的三個間隔定時器。這兩個syscall都是現在kernel/itimer.c文件中。?
7.7.3.1?getitimer()系統調用的實現?
函數sys_getitimer()有兩個參數:(1)which,指定查詢調用進程的哪一個間隔定時器,其取值可以是ITIMER_REAL、ITIMER_VIRT和ITIMER_PROF三者之一。(2)value指針,指向用戶空間中的一個itimerval結構,用于接收查詢結果。該函數的源碼如下:?
/*?SMP:?Only?we?modify?our?itimer?values.?*/?
asmlinkage?long?sys_getitimer(int?which,?struct?itimerval?*value)?
{?
int?error?=?-EFAULT;?
struct?itimerval?get_buffer;?
if?(value)?{?
error?=?do_getitimer(which,?&get_buffer);?
if?(!error?&&?
copy_to_user(value,?&get_buffer,?sizeof(get_buffer)))?
error?=?-EFAULT;?
}?
return?error;?
}?
顯然,sys_getitimer()函數主要通過do_getitimer()函數來查詢當前進程的間隔定時器信息,并將查詢結果保存在內核空間的結構變量get_buffer中。然后,調用copy_to_usr()宏將get_buffer中結果拷貝到用戶空間緩沖區中。?
函數do_getitimer()的源碼如下(kernel/itimer.c):?
int?do_getitimer(int?which,?struct?itimerval?*value)?
{?
register?unsigned?long?val,?interval;?
switch?(which)?{?
case?ITIMER_REAL:?
interval?=?current->it_real_incr;?
val?=?0;?
/*?
*?FIXME!?This?needs?to?be?atomic,?in?case?the?kernel?timer?happens!?
*/?
if?(timer_pending(¤t->real_timer))?{?
val?=?current->real_timer.expires?-?jiffies;?
/*?look?out?for?negative/zero?itimer..?*/?
if?((long)?val?<=?0)?
val?=?1;?
}?
break;?
case?ITIMER_VIRTUAL:?
val?=?current->it_virt_value;?
interval?=?current->it_virt_incr;?
break;?
case?ITIMER_PROF:?
val?=?current->it_prof_value;?
interval?=?current->it_prof_incr;?
break;?
default:?
return(-EINVAL);?
}?
jiffiestotv(val,?&value->it_value);?
jiffiestotv(interval,?&value->it_interval);?
return?0;?
}?
查詢的過程如下:?
(1)首先,用局部變量val和interval分別表示待查詢間隔定時器的間隔計數器的當前值和初始值。?
(2)如果which=ITIMER_REAL,則查詢當前進程的ITIMER_REAL間隔定時器。于是從current->it_real_incr中得到ITIMER_REAL間隔定時器的間隔計數器的初始值,并將其保存到interval局部變量中。而對于間隔計數器的當前值,由于ITITMER_REAL間隔定時器是通過real_timer這個內核動態定時器來實現的,因此不能通過current->it_real_value來獲得ITIMER_REAL間隔定時器的間隔計數器的當前值,而必須通過real_timer來得到這個值。為此先用timer_pending()函數來判斷current->real_timer是否已被起動。如果未啟動,則說明ITIMER_REAL間隔定時器也未啟動,因此其間隔計數器的當前值肯定是0。因此將val變量簡單地置0就可以了。如果已經啟動,則間隔計數器的當前值應該等于(timer_real.expires-jiffies)。?
(3)如果which=ITIMER_VIRT,則查詢當前進程的ITIMER_VIRT間隔定時器。于是簡單地將計數器初值it_virt_incr和當前值it_virt_value分別保存到局部變量interval和val中。?
(4)如果which=ITIMER_PROF,則查詢當前進程的ITIMER_PROF間隔定時器。于是簡單地將計數器初值it_prof_incr和當前值it_prof_value分別保存到局部變量interval和val中。?
(5)最后,通過轉換函數jiffiestotv()將val和interval轉換成timeval格式的時間值,并保存到value->it_value和value->it_interval中,作為查詢結果返回。?
7.7.3.2?setitimer()系統調用的實現?
函數sys_setitimer()不僅設置調用進程的指定間隔定時器,而且還返回該間隔定時器的原有信息。它有三個參數:(1)which,含義與sys_getitimer()中的參數相同。(2)輸入參數value,指向用戶空間中的一個itimerval結構,含有待設置的新值。(3)輸出參數ovalue,指向用戶空間中的一個itimerval結構,用于接收間隔定時器的原有信息。?
該函數的源碼如下(kernel/itimer.c):?
/*?SMP:?Again,?only?we?play?with?our?itimers,?and?signals?are?SMP?safe?
*?now?so?that?is?not?an?issue?at?all?anymore.?
*/?
asmlinkage?long?sys_setitimer(int?which,?struct?itimerval?*value,?
struct?itimerval?*ovalue)?
{?
struct?itimerval?set_buffer,?get_buffer;?
int?error;?
if?(value)?{?
if(copy_from_user(&set_buffer,?value,?sizeof(set_buffer)))?
return?-EFAULT;?
}?else?
memset((char?*)?&set_buffer,?0,?sizeof(set_buffer));?
error?=?do_setitimer(which,?&set_buffer,?ovalue???&get_buffer?:?0);?
if?(error?||?!ovalue)?
return?error;?
if?(copy_to_user(ovalue,?&get_buffer,?sizeof(get_buffer)))?
return?-EFAULT;?
return?0;?
}?
對該函數的注釋如下:?
(1)在輸入參數指針value非空的情況下,調用copy_from_user()宏將用戶空間中的待設置信息拷貝到內核空間中的set_buffer結構變量中。如果value指針為空,則簡單地將set_buffer結構變量全部置0。?
(2)調用do_setitimer()函數完成實際的設置操作。如果輸出參數ovalue指針有效,則以內核變量get_buffer的地址作為do_setitimer()函數的第三那個調用參數,這樣當do_setitimer()函數返回時,get_buffer結構變量中就將含有當前進程的指定間隔定時器的原來信息。Do_setitimer()函數返回0值表示成功,非0值表示失敗。?
(3)在do_setitimer()函數返回非0值的情況下,或者ovalue指針為空的情況下(不需要輸出間隔定時器的原有信息),函數就可以直接返回了。?
(4)如果ovalue指針非空,調用copy_to_user()宏將get_buffer()結構變量中值拷貝到ovalue所指向的用戶空間中去,以便讓用戶得到指定間隔定時器的原有信息值。?
函數do_setitimer()的源碼如下(kernel/itimer.c):?
int?do_setitimer(int?which,?struct?itimerval?*value,?struct?itimerval?*ovalue)?
{?
register?unsigned?long?i,?j;?
int?k;?
i?=?tvtojiffies(&value->it_interval);?
j?=?tvtojiffies(&value->it_value);?
if?(ovalue?&&?(k?=?do_getitimer(which,?ovalue))?<?0)?
return?k;?
switch?(which)?{?
case?ITIMER_REAL:?
del_timer_sync(¤t->real_timer);?
current->it_real_value?=?j;?
current->it_real_incr?=?i;?
if?(!j)?
break;?
if?(j?>?(unsigned?long)?LONG_MAX)?
j?=?LONG_MAX;?
i?=?j?+?jiffies;?
current->real_timer.expires?=?i;?
add_timer(¤t->real_timer);?
break;?
case?ITIMER_VIRTUAL:?
if?(j)?
j++;?
current->it_virt_value?=?j;?
current->it_virt_incr?=?i;?
break;?
case?ITIMER_PROF:?
if?(j)?
j++;?
current->it_prof_value?=?j;?
current->it_prof_incr?=?i;?
break;?
default:?
return?-EINVAL;?
}?
return?0;?
}?
對該函數的注釋如下:?
(1)首先調用tvtojiffies()函數將timeval格式的初始值和當前值轉換成以時鐘滴答為單位的時間值。并分別保存在局部變量i和j中。?
(2)如果ovalue指針非空,則調用do_getitimer()函數查詢指定間隔定時器的原來信息。如果do_getitimer()函數返回負值,說明出錯。因此就要直接返回錯誤值。否則繼續向下執行開始真正地設置指定的間隔定時器。?
(3)如果which=ITITMER_REAL,表示設置ITIMER_REAL間隔定時器。(a)調用del_timer_sync()函數(該函數在單CPU系統中就是del_timer()函數)將當前進程的real_timer定時器從內核動態定時器鏈表中刪除。(b)將it_real_incr和it_real_value分別設置為局部變量i和j。(c)如果j=0,說明不必啟動real_timer定時器,因此執行break語句退出switch…case控制結構,而直接返回。(d)將real_timer的expires成員設置成(jiffies+當前值j),然后調用add_timer()函數將當前進程的real_timer定時器加入到內核動態定時器鏈表中,從而啟動該定時器。?
(4)如果which=ITIMER_VIRT,則簡單地用局部變量i和j的值分別更新it_virt_incr和it_virt_value就可以了。?
(5)如果which=ITIMER_PROF,則簡單地用局部變量i和j的值分別更新it_prof_incr和it_prof_value就可以了。?
(6)最后,返回0值表示成功。?
7.7.3.3?alarm系統調用?
系統調用alarm可以讓調用進程在指定的秒數間隔后收到一個SIGALRM信號。它只有一個參數seconds,指定以秒數計的定時間隔。函數sys_alarm()的源碼如下(kernel/timer.c):?
/*?
*?For?backwards?compatibility??This?can?be?done?in?libc?so?Alpha?
*?and?all?newer?ports?shouldn't?need?it.?
*/?
asmlinkage?unsigned?long?sys_alarm(unsigned?int?seconds)?
{?
struct?itimerval?it_new,?it_old;?
unsigned?int?oldalarm;?
it_new.it_interval.tv_sec?=?it_new.it_interval.tv_usec?=?0;?
it_new.it_value.tv_sec?=?seconds;?
it_new.it_value.tv_usec?=?0;?
do_setitimer(ITIMER_REAL,?&it_new,?&it_old);?
oldalarm?=?it_old.it_value.tv_sec;?
/*?ehhh..?We?can't?return?0?if?we?have?an?alarm?pending..?*/?
/*?And?we'd?better?return?too?much?than?too?little?anyway?*/?
if?(it_old.it_value.tv_usec)?
oldalarm++;?
return?oldalarm;?
}?
這個系統調用實際上就是啟動進程的ITIMER_REAL間隔定時器。因此它完全可放到用戶空間的C函數庫(比如libc和glibc)中來實現。但是為了保此內核的向后兼容性,2.4.0版的內核仍然將這個syscall放在內核空間中來實現。函數sys_alarm()的實現過程如下:?
(1)根據參數seconds的值構造一個itimerval結構變量it_new。注意!由于alarm啟動的ITIMER_REAL間隔定時器是一次性而不是循環重復的,因此it_new變量中的it_interval成員一定要設置為0。?
(2)調用函數do_setitimer()函數以新構造的定時器it_new來啟動當前進程的ITIMER_REAL定時器,同時將該間隔定時器的原定時間隔保存到局部變量it_old中。?
(3)返回值oldalarm表示以秒數計的ITIMER_REAL間隔定時器的原定時間隔值。因此先把it_old.it_value.tv_sec賦給oldalarm,并且在it_old.it_value.tv_usec非0的情況下,將oldalarm的值加1(也即不足1秒補足1秒)。
?dreamice 回復于:2008-11-06 17:58:13
7.8?時間系統調用的實現?
本節講述與時間相關的syscall,這些系統調用主要用來供用戶進程向內核檢索當前時間與日期,因此他們是內核的時間服務接口。主要的時間系統調用共有5個:time、stime和gettimeofday、settimeofday,以及與網絡時間協議NTP相關的adjtimex系統調用。這里我們不關心NTP,因此僅分析前4個時間系統調用。前4個時間系統調用可以分為兩組:(1)time和stime是一組;(2)gettimeofday和settimeofday是一組。?
7.8.1?系統調用time和stime?
系統調用time()用于獲取以秒數表示的系統當前時間(即內核全局時間變量xtime中的tv_sec成員的值)。它只有一個參數——整型指針tloc,指向用戶空間中的一個整數,用來接收返回的當前時間值。函數sys_time()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_time(int?*?tloc)?
{?
int?i;?
/*?SMP:?This?is?fairly?trivial.?We?grab?CURRENT_TIME?and?
stuff?it?to?user?space.?No?side?effects?*/?
i?=?CURRENT_TIME;?
if?(tloc)?{?
if?(put_user(i,tloc))?
i?=?-EFAULT;?
}?
return?i;?
}?
注釋如下:?
(1)首先,函數調用CURRENT_TIME宏來得到以秒數表示的內核當前時間值,并將該值保存在局部變量i中。宏CURRENT_TIME定義在include/linux/sched.h頭文件中,它實際上就是內核全局時間變量xtime中的tv_sec成員。如下所示:?
#define?CURRENT_TIME?(xtime.tv_sec)?
(2)然后,在參數指針tloc非空的情況下將i的值通過put_user()宏傳遞到有tloc所指向的用戶空間中去,以作為函數的輸出結果。?
(3)最后,將局部變量I的值——也即也秒數表示的系統當前時間值作為返回值返回。?
系統調用stime()與系統調用time()剛好相反,它可以讓用戶設置系統的當前時間(以秒數為單位)。它同樣也只有一個參數——整型指針tptr,指向用戶空間中待設置的時間秒數值。函數sys_stime()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_stime(int?*?tptr)?
{?
int?value;?
if?(!capable(CAP_SYS_TIME))?
return?-EPERM;?
if?(get_user(value,?tptr))?
return?-EFAULT;?
write_lock_irq(&xtime_lock);?
xtime.tv_sec?=?value;?
xtime.tv_usec?=?0;?
time_adjust?=?0;?/*?stop?active?adjtime()?*/?
time_status?|=?STA_UNSYNC;?
time_maxerror?=?NTP_PHASE_LIMIT;?
time_esterror?=?NTP_PHASE_LIMIT;?
write_unlock_irq(&xtime_lock);?
return?0;?
}?
注釋如下:?
(1)首先檢查調用進程的權限,顯然,只有root用戶才能有權限修改系統時間。?
(2)調用get_user()宏將tptr指針所指向的用戶空間中的時間秒數值拷貝到內核空間中來,并保存到局部變量value中。?
(3)將局部變量value的值更新到全局時間變量xtime的tv_sec成員中,并將xtime的tv_usec成員清零。?
(4)在相應地重置其它狀態變量后,函數就可以返回了(返回值0表示成功)。?
7.8.2?系統調用gettimeofday?
這個syscall用來供用戶獲取timeval格式的當前時間信息(精確度為微秒級),以及系統的當前時區信息(timezone)。結構類型timeval的指針參數tv指向接受時間信息的用戶空間緩沖區,參數tz是一個timezone結構類型的指針,指向接收時區信息的用戶空間緩沖區。這兩個參數均為輸出參數,返回值0表示成功,返回負值表示出錯。函數sys_gettimeofday()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_gettimeofday(struct?timeval?*tv,?struct?timezone?*tz)?
{?
if?(tv)?{?
struct?timeval?ktv;?
do_gettimeofday(&ktv);?
if?(copy_to_user(tv,?&ktv,?sizeof(ktv)))?
return?-EFAULT;?
}?
if?(tz)?{?
if?(copy_to_user(tz,?&sys_tz,?sizeof(sys_tz)))?
return?-EFAULT;?
}?
return?0;?
}?
顯然,函數的實現主要分成兩個大的方面:?
(1)如果tv指針有效,則說明用戶要以timeval格式來檢索系統當前時間。為此,先調用do_gettimeofday()函數來檢索系統當前時間并保存到局部變量ktv中。然后再調用copy_to_user()宏將保存在內核空間中的當前時間信息拷貝到由參數指針tv所指向的用戶空間緩沖區中。?
(2)如果tz指針有效,則說明用戶要檢索當前時區信息,因此調用copy_to_user()宏將全局變量sys_tz中的時區信息拷貝到參數指針tz所指向的用戶空間緩沖區中。?
(3)最后,返回0表示成功。?
函數do_gettimeofday()的源碼如下(arch/i386/kernel/time.c):?
/*?
*?This?version?of?gettimeofday?has?microsecond?resolution?
*?and?better?than?microsecond?precision?on?fast?x86?machines?with?TSC.?
*/?
void?do_gettimeofday(struct?timeval?*tv)?
{?
unsigned?long?flags;?
unsigned?long?usec,?sec;?
read_lock_irqsave(&xtime_lock,?flags);?
usec?=?do_gettimeoffset();?
{?
unsigned?long?lost?=?jiffies?-?wall_jiffies;?
if?(lost)?
usec?+=?lost?*?(1000000?/?HZ);?
}?
sec?=?xtime.tv_sec;?
usec?+=?xtime.tv_usec;?
read_unlock_irqrestore(&xtime_lock,?flags);?
while?(usec?>=?1000000)?{?
usec?-=?1000000;?
sec++;?
}?
tv->tv_sec?=?sec;?
tv->tv_usec?=?usec;?
}?
該函數的完成實際的當前時間檢索工作。由于gettimeofday()系統調用要求時間精度要達到微秒級,因此do_gettimeofday()函數不能簡單地返回xtime中的值即可,而必須精確地確定自從時鐘驅動的Bottom?Half上一次更新xtime的那個時刻(由wall_jiffies變量表示,參見7.3節)到do_gettimeofday()函數的當前執行時刻之間的具體時間間隔長度,以便精確地修正xtime的值.如下圖7-9所示:?
假定被do_gettimeofday()用來修正xtime的時間間隔為fixed_usec,而從wall_jiffies到jiffies之間的時間間隔是lost_usec,而從jiffies到do_gettimeofday()函數的執行時刻的時間間隔是offset_usec。則下列三個等式成立:?
fixed_usec=(lost_usec+offset_usec)?
lost_usec=(jiffies-wall_jiffies)*TICK_SIZE=(jiffies-wall_jiffies)*(1000000/HZ)?
由于全局變量last_tsc_low表示上一次時鐘中斷服務函數timer_interrupt()執行時刻的CPU?TSC寄存器的值,因此我們可以用X86?CPU的TSC寄存器來計算offset_usec的值。也即:?
offset_usec=delay_at_last_interrupt+(current_tsc_low-last_tsc_low)*fast_gettimeoffset_quotient?
其中,delay_at_last_interrupt是從上一次發生時鐘中斷到timer_interrupt()服務函數真正執行時刻之間的時間延遲間隔。每一次timer_interrupt()被執行時都會計算這一間隔,并利用TSC的當前值更新last_tsc_low變量(可以參見7.4節)。假定current_tsc_low是do_gettimeofday()函數執行時刻TSC的當前值,全局變量fast_gettimeoffset_quotient則表示TSC寄存器每增加1所代表的時間間隔值,它是由time_init()函數所計算的。?
根據上述原理分析,do_gettimeofday()函數的執行步驟如下:?
(1)調用函數do_gettimeoffset()計算從上一次時鐘中斷發生到執行do_gettimeofday()函數的當前時刻之間的時間間隔offset_usec。?
(2)通過wall_jiffies和jiffies計算lost_usec的值。?
(3)然后,令sec=xtime.tv_sec,usec=xtime.tv_usec+lost_usec+offset_usec。顯然,sec表示系統當前時間在秒數量級上的值,而usec表示系統當前時間在微秒量級上的值。?
(4)用一個while{}循環來判斷usec是否已經溢出而超過106us=1秒。如果溢出,則將usec減去106us并相應地將sec增加1,直到usec不溢出為止。?
(5)最后,用sec和usec分別更新參數指針所指向的timeval結構變量。至此,整個查詢過程結束。?
函數do_gettimeoffset()根據CPU是否配置有TSC寄存器這一條件分別有不同的實現。其定義如下(arch/i386/kernel/time.c):?
#ifndef?CONFIG_X86_TSC?
static?unsigned?long?do_slow_gettimeoffset(void)?
{?
……?
}?
static?unsigned?long?(*do_gettimeoffset)(void)?=?do_slow_gettimeoffset;?
#else?
#define?do_gettimeoffset()?do_fast_gettimeoffset()?
#endif?
顯然,在配置有TSC寄存器的i386平臺上,do_gettimeoffset()函數實際上就是do_fast_gettimeoffset()函數。它通過TSC寄存器來計算do_fast_gettimeoffset()函數被執行的時刻到上一次時鐘中斷發生時的時間間隔值。其源碼如下(arch/i386/kernel/time.c):?
static?inline?unsigned?long?do_fast_gettimeoffset(void)?
{?
register?unsigned?long?eax,?edx;?
/*?Read?the?Time?Stamp?Counter?*/?
rdtsc(eax,edx);?
/*?..?relative?to?previous?jiffy?(32?bits?is?enough)?*/?
eax?-=?last_tsc_low;?/*?tsc_low?delta?*/?
/*?
*?Time?offset?=?(tsc_low?delta)?*?fast_gettimeoffset_quotient?
*?=?(tsc_low?delta)?*?(usecs_per_clock)?
*?=?(tsc_low?delta)?*?(usecs_per_jiffy?/?clocks_per_jiffy)?
*?
*?Using?a?mull?instead?of?a?divl?saves?up?to?31?clock?cycles?
*?in?the?critical?path.?
*/?
__asm__("mull?%2"?
:"=a"?(eax),?"=d"?(edx)?
:"rm"?(fast_gettimeoffset_quotient),?
"0"?(eax));?
/*?our?adjusted?time?offset?in?microseconds?*/?
return?delay_at_last_interrupt?+?edx;?
}?
對該函數的注釋如下:?
(1)先調用rdtsc()函數讀取當前時刻TSC寄存器的值,并將其高32位保存在edx局部變量中,低32位保存在局部變量eax中。?
(2)讓局部變量eax=Δtsc_low=eax-last_tsc_low;也即計算當前時刻的TSC值與上一次時鐘中斷服務函數timer_interrupt()執行時的TSC值之間的差值。?
(3)顯然,從上一次timer_interrupt()到當前時刻的時間間隔就是(Δtsc_low*fast_gettimeoffset_quotient)。因此用一條mul指令來計算這個乘法表達式的值。?
(4)返回值delay_at_last_interrupt+(Δtsc_low*fast_gettimeoffset_quotient)就是從上一次時鐘中斷發生時到當前時刻之間的時間偏移間隔值。?
7.8.3?系統調用settimeofday?
這個系統調用與gettimeofday()剛好相反,它供用戶設置當前時間以及當前時間信息。它也有兩個參數:(1)參數指針tv,指向含有待設置時間信息的用戶空間緩沖區;(2)參數指針tz,指向含有待設置時區信息的用戶空間緩沖區。函數sys_settimeofday()的源碼如下(kernel/time.c):?
asmlinkage?long?sys_settimeofday(struct?timeval?*tv,?struct?timezone?*tz)?
{?
struct?timeval?new_tv;?
struct?timezone?new_tz;?
if?(tv)?{?
if?(copy_from_user(&new_tv,?tv,?sizeof(*tv)))?
return?-EFAULT;?
}?
if?(tz)?{?
if?(copy_from_user(&new_tz,?tz,?sizeof(*tz)))?
return?-EFAULT;?
}?
return?do_sys_settimeofday(tv???&new_tv?:?NULL,?tz???&new_tz?:?NULL);?
}?
函數首先調用copy_from_user()宏將保存在用戶空間中的待設置時間信息和時區信息拷貝到內核空間中來,并保存到局部變量new_tv和new_tz中。然后,調用do_sys_settimeofday()函數完成實際的時間設置和時區設置操作。?
函數do_sys_settimeofday()的源碼如下(kernel/time.c):?
int?do_sys_settimeofday(struct?timeval?*tv,?struct?timezone?*tz)?
{?
static?int?firsttime?=?1;?
if?(!capable(CAP_SYS_TIME))?
return?-EPERM;?
if?(tz)?{?
/*?SMP?safe,?global?irq?locking?makes?it?work.?*/?
sys_tz?=?*tz;?
if?(firsttime)?{?
firsttime?=?0;?
if?(!tv)?
warp_clock();?
}?
}?
if?(tv)?
{?
/*?SMP?safe,?again?the?code?in?arch/foo/time.c?should?
*?globally?block?out?interrupts?when?it?runs.?
*/?
do_settimeofday(tv);?
}?
return?0;?
}?
該函數的執行過程如下:?
(1)首先,檢查調用進程是否有相應的權限。如果沒有,則返回錯誤值-EPERM。?
(2)如果執政tz有效,則用tz所指向的新時區信息更新全局變量sys_tz。并且如果是第一次設置時區信息,則在tv指針不為空的情況下調用wrap_clock()函數來調整xtime中的秒數值。函數wrap_clock()的源碼如下(kernel/time.c):?
inline?static?void?warp_clock(void)?
{?
write_lock_irq(&xtime_lock);?
xtime.tv_sec?+=?sys_tz.tz_minuteswest?*?60;?
write_unlock_irq(&xtime_lock);?
}?
(3)如果參數tv指針有效,則根據tv所指向的新時間信息調用do_settimeofday()函數來更新內核的當前時間xtime。?
(4)最后,返回0值表示成功。?
函數do_settimeofday()執行剛好與do_gettimeofday()相反的操作。這是因為全局變量xtime所表示的時間是與wall_jiffies相對應的那一個時刻。因此,必須從參數指針tv所指向的新時間中減去時間間隔fixed_usec(其含義見7.8.2節)。函數源碼如下(arch/i386/kernel/time.c):?
void?do_settimeofday(struct?timeval?*tv)?
{?
write_lock_irq(&xtime_lock);?
/*?
*?This?is?revolting.?We?need?to?set?"xtime"?correctly.?However,?the?
*?value?in?this?location?is?the?value?at?the?most?recent?update?of?
*?wall?time.?Discover?what?correction?gettimeofday()?would?have?
*?made,?and?then?undo?it!?
*/?
tv->tv_usec?-=?do_gettimeoffset();?
tv->tv_usec?-=?(jiffies?-?wall_jiffies)?*?(1000000?/?HZ);?
while?(tv->tv_usec?<?0)?{?
tv->tv_usec?+=?1000000;?
tv->tv_sec--;?
}?
xtime?=?*tv;?
time_adjust?=?0;?/*?stop?active?adjtime()?*/?
time_status?|=?STA_UNSYNC;?
time_maxerror?=?NTP_PHASE_LIMIT;?
time_esterror?=?NTP_PHASE_LIMIT;?
write_unlock_irq(&xtime_lock);?
}?
該函數的執行步驟如下:?
(1)調用do_gettimeoffset()函數計算上一次時鐘中斷發生時刻到當前時刻之間的時間間隔值。?
(2)通過wall_jiffies與jiffies計算二者之間的時間間隔lost_usec。?
(3)從tv->tv_usec中減去fixed_usec,即:tv->tv_usec-=(lost_usec+offset_usec)。?
(4)用一個while{}循環根據tv->tv_usec是否小于0來調整tv結構變量。如果tv->tv_usec小于0,則將tv->tv_usec加上106us,并相應地將tv->tv_sec減1。直到tv->tv_usec不小于0為止。?
(5)用修正后的時間tv來更新內核全局時間變量xtime。?
(6)最后,重置其它時間狀態變量。?
至此,我們已經完全分析了整個Linux內核的時鐘機制!
總結
以上是生活随笔為你收集整理的Linux内核的时钟中断的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 神舟十四号成功升空意味啥?看完才发现”上
- 下一篇: 一人一张照片!特斯拉将带3万名车主“上天