Linux核心的時鐘中斷機制
第七章 Linux核心的時鐘中斷
(By 詹榮開,NUDT)
opyright © 2003 by 詹榮開
E-mail:[email protected]
Linux-2.4.0
Version 1.0.0,2003-2-14
摘要:本文主要從核心實作的角度分析了Linux 2.4.0核心的時鐘中斷、核心對時間的表示等。本文是為那些想要了解Linux I/O子系統的讀者和Linux驅動程式開發人員而寫的。
關鍵詞:Linux、時鐘、定時器
申明:這份文檔是按照自由軟體開放源代碼的精神釋出的,任何人可以免費獲得、使用和重新釋出,但是你沒有限制别人重新釋出你釋出内容的權利。釋出本文的目的是希望它能對讀者有用,但沒有任何擔保,甚至沒有适合特定目的的隐含的擔保。更詳細的情況請參閱GNU通用公共許可證(GPL),以及GNU自由文檔協定(GFDL)。
你應該已經和文檔一起收到一份GNU通用公共許可證(GPL)的副本。如果還沒有,寫信給:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA
歡迎各位指出文檔中的錯誤與疑問。
前言
時間在一個作業系統核心中占據着重要的地位,它是驅動一個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,選擇對那個計數器進行操作。“00”表示選擇Counter 0,“01”表示選擇Counter 1,“10”表示選擇Counter 2,“11”表示Read-Back Command(僅對于8254,對于8253無效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示鎖存(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,作業系統通常可以得到更為精準的時間度量。假如clock-cycle的頻率是400MHZ,那麼TSC就将每2.5納秒增加一次。
7.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
#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
# 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
# define RTC_RATE_SELECT 0x0F
正如7.1.1.1節所介紹的那樣,bit[6:4]有5中可能的取值,分别為除法器選擇不同的工作頻率或用于重置除法器,各種可能的取值如下定義所示:
# define RTC_REF_CLCK_4MHZ 0x00
# define RTC_REF_CLCK_1MHZ 0x10
# define RTC_REF_CLCK_32KHZ 0x20
# 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
# define RTC_PIE 0x40
# define RTC_AIE 0x20
# define RTC_UIE 0x10
# define RTC_SQWE 0x08
# define RTC_DM_BINARY 0x04
# define RTC_24H 0x02
# define RTC_DST_EN 0x01
寄存器C是RTC晶片的中斷請求狀态寄存器,Linux用宏别名RTC_INTR_FLAGS來表示寄存器C,它與其中的各标志位的定義如下所示:
#define RTC_INTR_FLAGS RTC_REG_C
# define RTC_IRQF 0x80
# 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
(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。控制寄存器A中的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子產品所調用。其源碼如下:
unsigned long get_cmos_time(void)
{
unsigned int year, mon, day, hour, min, sec;
int i;
for (i = 0 ; i < 1000000 ; i++)
if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)
break;
for (i = 0 ; i < 1000000 ; i++)
if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))
break;
do {
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)) {
mon += 12;
year -= 1;
}
return (((
(unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +
year*365 - 719499
)*24 + hour
)*60 + min
)*60 + sec;
}
(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;
spin_lock(&rtc_lock);
save_control = CMOS_READ(RTC_CONTROL);
CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL);
save_freq_select = CMOS_READ(RTC_FREQ_SELECT);
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);
real_seconds = nowtime % 60;
real_minutes = nowtime / 60;
if (((abs(real_minutes - cmos_minutes) + 15)/30) & 1)
real_minutes += 30;
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;
}
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檔案中:
static long last_rtc_update;
每一次成功地調用set_rtc_mmss()函數後,核心都會馬上将last_rtc_update更新為目前時間(具體請見7.4.3節)。
7.3 Linux對時間的表示
通常,作業系統可以使用三種方法來表示系統的目前時間與日期:①最簡單的一種方法就是直接用一個64位的計數器來對時鐘滴答進行計數。②第二種方法就是用一個32位計數器來對秒進行計數,同時還用一個32位的輔助計數器對時鐘滴答計數,之子累積到一秒為止。因為232超過136年,是以這種方法直至22世紀都可以讓系統工作得很好。③第三種方法也是按時鐘滴答進行計數,但是是相對于系統啟動以來的滴答次數,而不是相對于相對于某個确定的外部時刻;當讀外部後備時鐘(如RTC)或使用者輸入實際時間時,根據目前的滴答次數計算系統目前時間。
UNIX類作業系統通常都采用第三種方法來維護系統的時間與日期。
7.3.1 基本概念
首先,有必要明确一些Linux核心時鐘驅動中的基本概念。
(1)時鐘周期(clock cycle)的頻率:8253/8254 PIT的本質就是對由晶體振蕩器産生的時鐘周期進行計數,晶體振蕩器在1秒時間内産生的時鐘脈沖個數就是時鐘周期的頻率。Linux用宏CLOCK_TICK_RATE來表示8254 PIT的輸入時鐘脈沖的頻率(在PC機中這個值通常是1193180HZ),該宏定義在include/asm-i386/timex.h頭檔案中:
#define CLOCK_TICK_RATE 1193180
(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;
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):
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ)
類似地,被除數表達式中的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;
suseconds_t tv_usec;
};
其中,成員tv_sec表示目前時間距UNIX時間基準的秒數值,而成員tv_usec則表示一秒之内的微秒值,且1000000>tv_usec>=0。
Linux核心通過timeval結構類型的全局變量xtime來維持目前時間,該變量定義在kernel/timer.c檔案中,如下所示:
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檔案中:
unsigned long wall_jiffies;
③全局變量sys_tz:它是一個timezone結構類型的全局變量,表示系統目前的時區資訊。結構類型timezone定義在include/linux/time.h頭檔案中,如下所示:
struct timezone {
int tz_minuteswest;
int tz_dsttime;
};
基于上述結構,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):
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):
static int delay_at_last_interrupt;
關于delay_at_last_interrupt的計算步驟我們将在分析timer_interrupt()函數時讨論。
(2)全局變量last_tsc_low:它表示中斷服務timer_interrupt真正在CPU上執行時刻的TSC寄存器值的低32位(LSB)。
顯然,通過delay_at_last_interrupt、last_tsc_low和時刻x處的TSC寄存器值,我們就可以完全确定時刻x距上一次時鐘中斷産生時刻的時間間隔偏移offset_usec的值。實作在arch/i386/kernel/time.c中的函數do_fast_gettimeoffset()就是這樣計算時間間隔偏移的,當然它僅在CPU配置有TSC寄存器時才被使用,後面我們會詳細分析這個函數。
7.4 時鐘中斷的驅動
如前所述,8253/8254 PIT的通道0通常被用來在IRQ0上産生周期性的時鐘中斷。對時鐘中斷的驅動是絕大數作業系統核心實作time-keeping的關鍵所在。不同的OS對時鐘驅動的要求也不同,但是一般都包含下列要求内容:
1. 維護系統的目前時間與日期。
2. 防止程序運作時間超出其允許的時間。
3. 對CPU的使用情況進行記帳統計。
4. 處理使用者程序發出的時間系統調用。
5. 對系統某些部分提供監視定時器。
其中,第一項功能是所有OS都必須實作的基礎功能,它是OS核心的運作基礎。通常有三種方法可用來維護系統的時間與日期:(1)最簡單的一種方法就是用一個64位的計數器來對時鐘滴答進行計數。(2)第二種方法就是用一個32位計數器來對秒進行計數。用一個32位的輔助計數器來對時鐘滴答計數直至累計一秒為止。因為232超過136年,是以這種方法直至22世紀都可以工作得很好。(3)第三種方法也是按滴答進行計數,但卻是相對于系統啟動以來的滴答次數,而不是相對于一個确定的外部時刻。當讀後備時鐘(如RTC)或使用者輸入實際時間時,根據目前的滴答次數計算系統目前時間。
UNIX類的OS通常都采用第三種方法來維護系統的時間與日期。
7.4.1 Linux對時鐘中斷的初始化
Linux對時鐘中斷的初始化是分為幾個步驟來進行的:(1)首先,由init_IRQ()函數通過調用init_ISA_IRQ()函數對中斷向量32~256所對應的中斷向量描述符進行初始化設定。顯然,這其中也就把IRQ0(也即中斷向量32)的中斷向量描述符初始化了。(2)然後,init_IRQ()函數設定中斷向量32~256相對應的中斷門。(3)init_IRQ()函數對PIT進行初始化程式設計;(4)sched_init()函數對計數器、時間中斷的Bottom Half進行初始化。(5)最後,由time_init()函數對Linux核心的時鐘中斷機制進行初始化。這三個初始化函數都是由init/main.c檔案中的start_kernel()函數調用的,如下:
asmlinkage void __init start_kernel()
{
…
trap_init();
init_IRQ();
sched_init();
time_init();
softirq_init();
…
}
(1)init_IRQ()函數對8254 PIT的初始化程式設計
函數init_IRQ()函數在完成中斷門的初始化後,就對8254 PIT進行初始化程式設計設定,設定的步驟如下:(1)設定8254 PIT的控制寄存器(端口0x43)的值為“01100100”,也即選擇通道0、先讀寫LSB再讀寫MSB、工作模式2、二進制存儲格式。(2)将宏LATCH的值寫入通道0的計數器中(端口0x40),注意要先寫LATCH的LSB,再寫LATCH的高位元組。其源碼如下所示(arch/i386/kernel/i8259.c):
void __init init_IRQ(void)
{
……
outb_p(0x34,0x43);
outb_p(LATCH & 0xff , 0x40);
outb(LATCH >> 8 , 0x40);
……
}
(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;
dodgy_tsc();
if (cpu_has_tsc) {
unsigned long tsc_quotient = calibrate_tsc();
if (tsc_quotient) {
fast_gettimeoffset_quotient = tsc_quotient;
use_tsc = 1;
x86_udelay_tsc = 1;
#ifndef do_gettimeoffset
do_gettimeoffset = do_fast_gettimeoffset;
#endif
do_get_fast_time = do_gettimeofday;
{ 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");
co_cpu_write(CO_CPU_TIMEVAL, CO_TIME_HZ/HZ);
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) | CO_CTRL_TIMERUN);
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) & ~CO_CTRL_TIMEMASK);
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檔案中,如下:
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):
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int count;
write_lock(&xtime_lock);
if (use_tsc)
{
rdtscl(last_tsc_low);
spin_lock(&i8253_lock);
outb_p(0x00, 0x43);
count = inb_p(0x40);
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):
static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
。。。。。。
do_timer(regs);
。。。。。。。
if ((time_status & STA_UNSYNC) == 0 &&
xtime.tv_sec > last_rtc_update + 660 &&
xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 &&
xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) {
if (set_rtc_mmss(xtime.tv_sec) == 0)
last_rtc_update = xtime.tv_sec;
else
last_rtc_update = xtime.tv_sec - 600;
}
……
}
上述代碼中省略了許多與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
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):
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) {
if (!(psecs % HZ))
send_sig(SIGXCPU, p, 1);
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;
}
}
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):
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;
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):
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):
static void update_wall_time_one_tick(void)
{
if ( (time_adjust_step = time_adjust) != 0 ) {
if (time_adjust > tickadj)
time_adjust_step = tickadj;
else if (time_adjust < -tickadj)
time_adjust_step = -tickadj;
time_adjust -= time_adjust_step;
}
xtime.tv_usec += tick + time_adjust_step;
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;
}
}
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檔案中,如下述代碼段所示:
#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的一部分。基于資料結構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):
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)
{
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) {
vec = tv1.vec + tv1.index;
} else if (idx <= 0xffffffffUL) {
int i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
vec = tv5.vec + i;
} else {
INIT_LIST_HEAD(&timer->list);
return;
}
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)
{
struct list_head *head, *curr, *next;
head = tv->vec + tv->index;
curr = head->next;
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循環開始下一次定時器服務。
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;
struct timeval it_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結構,用于接收查詢結果。該函數的源碼如下:
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;
if (timer_pending(¤t->real_timer)) {
val = current->real_timer.expires - jiffies;
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):
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):
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;
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秒)。
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;
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;
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):
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;
rdtsc(eax,edx);
eax -= last_tsc_low;
__asm__("mull %2"
:"=a" (eax), "=d" (edx)
:"rm" (fast_gettimeoffset_quotient),
"0" (eax));
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) {
sys_tz = *tz;
if (firsttime) {
firsttime = 0;
if (!tv)
warp_clock();
}
}
if (tv)
{
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);
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;
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核心的時鐘機制!