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 /* 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 是待寫入寄存器的值:闞 TC_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)= (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) 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 寄存器時才被使用,後面我們會詳細分析這個函數。
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 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) last_rtc_update + 660 &&
xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 &&
xtime.tv_usec 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 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() 將更新指定進程的總時間統計信息』
ask_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;
}
}
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 > SHIFT_SCALE;
time_phase += ltemp = FINEUSEC) {
long ltemp = time_phase >> SHIFT_SCALE;
time_phase -= ltemp 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_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 個定時器向量』
56≤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 >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 expires;
unsigned long idx = expires - timer_jiffies;
struct list_head * vec;
if (idx > TVR_BITS) & TVN_MASK;
vec = tv2.vec + i;
} else if (idx > (TVR_BITS + TVN_BITS)) & TVN_MASK;
vec = tv3.vec + i;
} else if (idx > (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
vec = tv4.vec + i;
} else if ((signed long) idx > (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 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 導醯 ? 時,我們就說該間隔定時器到期。與上一節所說的內核動態定時器相比,二
者最大的區別在於定時器的計時方式不同。內核定時器是通過它的到期時刻 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 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 就可以了
imer_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' 還有問題請來論壇尋求幫助: http://www.xxlinux.com/bbs/