3.1 基本概念
首先,有必要明確一些Linux內核時鐘驅動中的基本概念。
(1)時鐘週期(clock cycle)的頻率:8253/8254 PIT的本質就是對由晶體振盪器產生的時鐘週期進行計數,晶體振盪器在1秒時間內產生的時鐘脈衝個數就是時鐘週期的頻率。Linux用宏 CLOCK_TICK_RATE來表示8254 PIT的輸入時鐘脈衝的頻率(在PC機中這個值一般是1193180HZ),該宏定義在include/asm-i386/timex.h頭文件中:
#define CLOCK_TICK_RATE 1193180 /* Underlying HZ */
(2)時鐘滴答(clock tick):咱們知道,當PIT通道0的計數器減到0值時,它就在IRQ0上產生一次時鐘中斷,也即一次時鐘滴答。PIT通道0的計數器的初始值決定了要過多少時鐘週期才產生一次時鐘中斷,所以也就決定了一次時鐘滴答的時間間隔長度。
(3)時鐘滴答的頻率(HZ):也即1秒時間內PIT所產生的時鐘滴答次數。相似地,這個值也是由PIT通道0的計數器初值決定的(反過來講,肯定了時鐘 滴答的頻率值後也就能夠肯定8254 PIT通道0的計數器初值)。Linux內核用宏HZ來表示時鐘滴答的頻率,並且在不一樣的平臺上HZ有不一樣的定義值。對於ALPHA和IA62平臺HZ的 值是1024,對於SPARC、MIPS、ARM和i386等平臺HZ的值都是100。該宏在i386平臺上的定義以下(include/asm- i386/param.h):
#ifndef HZ
#define HZ 100
#endif
根據HZ的值,咱們也能夠知道一次時鐘滴答的具體時間間隔應該是(1000ms/HZ)=10ms。
(4)時鐘滴答的時間間隔:Linux用全局變量tick來表示時鐘滴答的時間間隔長度,該變量定義在kernel/timer.c文件中,以下:
long tick = (1000000 + HZ/2) / HZ; /* timer interrupt period */
tick變量的單位是微妙(μs),因爲在不一樣平臺上宏HZ的值會有所不一樣,所以方程式tick=1000000÷HZ的結果可能會是個小數,所以將其進 行四捨五入成一個整數,因此Linux將tick定義成(1000000+HZ/2)/HZ,其中被除數表達式中的HZ/2的做用就是用來將tick值向 上圓整成一個整型數。
另外,Linux還用宏TICK_SIZE來做爲tick變量的引用別名(alias),其定義以下(arch/i386/kernel/time.c):
#define TICK_SIZE tick
(5)宏LATCH:Linux用宏LATCH來定義要寫到PIT通道0的計數器中的值,它表示PIT將沒隔多少個時鐘週期產生一次時鐘中斷。顯然LATCH應該由下列公式計算:
LATCH=(1秒以內的時鐘週期個數)÷(1秒以內的時鐘中斷次數)=(CLOCK_TICK_RATE)÷(HZ)
相似地,上述公式的結果可能會是個小數,應該對其進行四捨五入。因此,Linux將LATCH定義爲(include/linux/timex.h):
/* LATCH is used in the interval timer and ftape setup. */
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
相似地,被除數表達式中的HZ/2也是用來將LATCH向上圓整成一個整數。 linux
3.2 表示系統當前時間的內核數據結構
做爲一種UNIX類操做系統,Linux內核顯然採用本節一開始所述的第三種方法來表示系統的當前時間。Linux內核在表示系統當前時間時用到了三個重要的數據結構:
①全局變量jiffies:這是一個32位的無符號整數,用來表示自內核上一次啓動以來的時鐘滴答次數。每發生一次時鐘滴答,內核的時鐘中斷處理函數 timer_interrupt()都要將該全局變量jiffies加1。該變量定義在kernel/timer.c源文件中,以下所示:
unsigned long volatile jiffies;
C語言限定符volatile表示jiffies是一個易改變的變量,所以編譯器將使對該變量的訪問從不經過CPU內部cache來進行。
②全局變量xtime:它是一個timeval結構類型的變量,用來表示當前時間距UNIX時間基準1970-01-01 00:00:00的相對秒數值。結構timeval是Linux內核表示時間的一種格式(Linux內核對時間的表示有多種格式,每種格式都有不一樣的時間 精度),其時間精度是微秒。該結構是內核表示時間時最經常使用的一種格式,它定義在頭文件include/linux/time.h中,以下所示:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
其中,成員tv_sec表示當前時間距UNIX時間基準的秒數值,而成員tv_usec則表示一秒以內的微秒值,且1000000>tv_usec>=0。
Linux內核經過timeval結構類型的全局變量xtime來維持當前時間,該變量定義在kernel/timer.c文件中,以下所示:
/* The current time */
volatile struct timeval xtime __attribute__ ((aligned (16)));
可是,全局變量xtime所維持的當前時間一般是供用戶來檢索和設置的,而其餘內核模塊一般不多使用它(其餘內核模塊用得最多的是jiffies),所以 對xtime的更新並非一項緊迫的任務,因此這一工做一般被延遲到時鐘中斷的底半部分(bottom half)中來進行。因爲bottom half的執行時間帶有不肯定性,所以爲了記住內核上一次更新xtime是何時,Linux內核定義了一個相似於jiffies的全局變量 wall_jiffies,來保存內核上一次更新xtime時的jiffies值。時鐘中斷的底半部分每一次更新xtime的時侯都會將 wall_jiffies更新爲當時的jiffies值。全局變量wall_jiffies定義在kernel/timer.c文件中:
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
③全局變量sys_tz:它是一個timezone結構類型的全局變量,表示系統當前的時區信息。結構類型timezone定義在include/linux/time.h頭文件中,以下所示:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of dst correction */
};
基於上述結構,Linux在kernel/time.c文件中定義了全局變量sys_tz表示系統當前所處的時區信息,以下所示:
struct timezone sys_tz; 編程
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寄存器。 緩存
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") app
#define rdtscll(val) \
__asm__ __volatile__ ("rdtsc" : "=A" (val))
宏rdtsc()同時讀取TSC的LSB與MSB,並分別保存到宏參數low和high中。宏rdtscl則只讀取TSC寄存器的LSB,並保存到宏參數low中。宏rdtscll讀取TSC的當前64位值,並將其保存到宏參數val這個64位變量中。 ide
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內核能夠實現微秒級的高精度定時服務,也便可以肯定兩次時鐘中斷之間的某個時刻的微秒級時間值。要肯定時刻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寄存器時才被使用,後面咱們會詳細分析這個函 數。 this
四、 時鐘中斷的驅動
如前所述,8253/8254 PIT的通道0一般被用來在IRQ0上產生週期性的時鐘中斷。對時鐘中斷的驅動是絕大數操做系統內核實現time-keeping的關鍵所在。不一樣的OS對時鐘驅動的要求也不一樣,可是通常都包含下列要求內容:
1. 維護系統的當前時間與日期。
2. 防止進程運行時間超出其容許的時間。
3. 對CPU的使用狀況進行記賬統計。
4. 處理用戶進程發出的時間系統調用。
5. 對系統某些部分提供監視定時器。
其中,第一項功能是全部OS都必須實現的基礎功能,它是OS內核的運行基礎。
UNIX類的OS一般都經過計算系統啓動以來的滴答次數的方法來維護系統的時間與日期。當讀後備時鐘(如RTC)或用戶輸入實際時間時,根據當前的滴答次數計算系統當前時間。 操作系統
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)將宏 LATCH的值寫入通道0的計數器中(端口0x40),注意要先寫LATCH的LSB,再寫LATCH的高字節。其源碼以下所示(arch/i386 /kernel/i8259.c):
void __init init_IRQ(void)
{
……
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
……
}
(2)sched_init()對定時器機制和時鐘中斷的Bottom Half的初始化
函數sched_init()中與時間相關的初始化過程主要有兩步:(1)調用init_timervecs()函數初始化內核定時器機制;(2)調用 init_bh()函數將BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所對應的BH函數分別設置成timer_bh()、 tqueue_bh()和immediate_bh()函數。以下所示(kernel/sched.c):
void __init sched_init(void)
{
……
init_timervecs();
init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
……
}
(3)time_init()函數對內核時鐘中斷機制的初始化
前面兩個函數所進行的初始化步驟都是爲時間中斷機制作好準備而已。在執行完init_IRQ()函數和sched_init()函數後,CPU已經能夠爲 IRQ0上的時鐘中斷進行服務了,由於IRQ0所對應的中斷門已經被設置好指向中斷服務函數IRQ0x20_interrupt()。可是因爲此時中斷向 量0x20的中斷向量描述符irq_desc[0]仍是處於初始狀態(其status成員的值爲IRQ_DISABLED),並未掛接任何具體的中斷服務 描述符,所以這時CPU對IRQ0的中斷服務並無任何具體意義,而只是按照規定的流程空跑一趟。可是當CPU執行完time_init()函數後,情形 就大不同了。
函數time_init()主要作三件事:(1)從RTC中獲取內核啓動時的時間與日期;(2)在CPU有TSC的狀況下校準TSC,以便爲後面使用 TSC作好準備;(3)在IRQ0的中斷請求描述符中掛接具體的中斷服務描述符。其源碼以下所示(arch/i386/kernel/time.c):
void __init time_init(void)
{
extern int x86_udelay_tsc;
xtime.tv_sec = get_cmos_time();
xtime.tv_usec = 0;
/*
* If we have APM enabled or the CPU clock speed is variable
* (CPU stops clock on HLT or slows clock to save power)
* then the TSC timestamps may diverge by up to 1 jiffy from
* 'real time' but nothing will break.
* The most frequent case is that the CPU is "woken" from a halt
* state by the timer interrupt itself, so we get 0 error. In the
* rare cases where a driver would "wake" the CPU and request a
* timestamp, the maximum error is < 1 jiffy. But timestamps are
* still perfectly ordered.
* Note that the TSC counter will be reset if APM suspends
* to disk; this won't break the kernel, though, 'cuz we're
* smart. See arch/i386/kernel/apm.c.
*/
/*
* Firstly we have to do a CPU check for chips with
* a potentially buggy TSC. At this point we haven't run
* the ident/bugs checks so we must run this hook as it
* may turn off the TSC flag.
*
* NOTE: this doesnt yet handle SMP 486 machines where only
* some CPU's have a TSC. Thats never worked and nobody has
* moaned if you have the only one in the world - you fix it!
*/
dodgy_tsc();
if (cpu_has_tsc) {
unsigned long tsc_quotient = calibrate_tsc();
if (tsc_quotient) {
fast_gettimeoffset_quotient = tsc_quotient;
use_tsc = 1;
/*
* We could be more selective here I suspect
* and just enable this for the next intel chips ?
*/
x86_udelay_tsc = 1;
#ifndef do_gettimeoffset
do_gettimeoffset = do_fast_gettimeoffset;
#endif
do_get_fast_time = do_gettimeofday;
/* report CPU clock rate in Hz.
* The formula is (10^6 * 2^32) / (2^32 * 1 / (clocks/us)) =
* clock/second. Our precision is about 100 ppm.
*/
{ unsigned long eax=0, edx=1000;
__asm__("divl %2"
:"=a" (cpu_khz), "=d" (edx)
:"r" (tsc_quotient),
"0" (eax), "1" (edx));
printk("Detected %lu.%03lu MHz processor.\n", cpu_khz / 1000, cpu_khz % 1000);
}
}
}
#ifdef CONFIG_VISWS
printk("Starting Cobalt Timer system clock\n");
/* Set the countdown value */
co_cpu_write(CO_CPU_TIMEVAL, CO_TIME_HZ/HZ);
/* Start the timer */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) | CO_CTRL_TIMERUN);
/* Enable (unmask) the timer interrupt */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) & ~CO_CTRL_TIMEMASK);
/* Wire cpu IDT entry to s/w handler (and Cobalt APIC to IDT) */
setup_irq(CO_IRQ_TIMER, &irq0);
#else
setup_irq(0, &irq0);
#endif
}
對該函數的註解以下:
(1)調用函數get_cmos_time()從RTC中獲得系統啓動時的時間與日期,它返回的是當前時間相對於1970-01-01 00:00:00這個UNIX時間基準的秒數值。所以這個秒數值就被保存在系統全局變量xtime的tv_sec成員中。而xtime的另外一個成員 tv_usec則被初始化爲0。
(2)經過dodgy_tsc()函數檢測CPU是否存在時間戳記數器BUG(I know nothing about it:-)
(3)經過宏cpu_has_tsc來肯定系統中CPU是否存在TSC計數器。若是存在TSC,那麼內核就能夠用TSC來得到更爲精確的時間。爲了可以用 TSC來修正內核時間。這裏必須做一些初始化工做:①調用calibrate_tsc()來肯定TSC的每一次計數真正表明多長的時間間隔(單位爲 us),也即一個時鐘週期的真正時間間隔長度。②將calibrate_tsc()函數所返回的值保存在全局變量 fast_gettimeoffset_quotient中,該變量被用來快速地計算時間誤差;同時還將另外一個全局變量use_tsc設置爲1,表示內核 可使用TSC。這兩個變量都定義在arch/i386/kernel/time.c文件中,以下:
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
……
static int use_tsc;
③接下來,將系統全局變量x86_udelay_tsc設置爲1,表示能夠經過TSC來實現微妙級的精確延時。該變量定義在arch/i386/lib /delay.c文件中。④將函數指針do_gettimeoffset強制性地指向函數do_fast_gettimeoffset()(與之對應的是 do_slow_gettimeoffset()函數),從而使內核在計算時間誤差時能夠用TSC這種快速的方法來進行。⑤將函數指針 do_get_fast_time指向函數do_gettimeofday(),從而可讓其餘內核模塊經過do_gettimeofday()函數來獲 得更精準的當前時間。⑥計算並報告根據TSC所算得的CPU時鐘頻率。
(4)不考慮CONFIG_VISWS的狀況,所以time_init()的最後一個步驟就是調用setup_irq()函數來爲IRQ0掛接具體的中斷 服務描述符irq0。全局變量irq0是時鐘中斷請求的中斷服務描述符,其定義以下(arch/i386/kernel/time.c):
static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
顯然,函數timer_interrupt()將成爲時鐘中斷的服務程序(ISR),而SA_INTERRUPT標誌也指定了 timer_interrupt()函數將是在CPU關中斷的條件下執行的。結構irq0中的next指針被設置爲NULL,所以IRQ0所對應的中斷服 務隊列中只有irq0這惟一的一個元素,且IRQ0不容許中斷共享。
4.2 時鐘中斷服務例程timer_interrupt()
中斷服務描述符irq0一旦被鉤掛到IRQ0的中斷服務隊列中去後,Linux內核就能夠經過irq0->handler函數指針所指向的 timer_interrupt()函數對時鐘中斷請求進行真正的服務,而不是向前面所說的那樣只是讓CPU「空跑」一趟。此時,Linux內核能夠說是 真正的「跳動」起來了。
在本節一開始所述的對時鐘中斷驅動的5項要求中,一般只有第一項(即timekeeping)是最爲迫切的,所以必須在時鐘中斷服務例程中完成。而其他的 幾個要求能夠稍緩,所以能夠放在時鐘中斷的Bottom Half中去執行。這樣,Linux內核就是timer_interrupt()函數的執行時間儘量的短,由於它是在CPU關中斷的條件下執行的。
函數timer_interrupt()的源碼以下(arch/i386/kernel/time.c):
/*
* This is the same as the above, except we _also_ save the current
* Time Stamp Counter value at the time of the timer interrupt, so that
* we later on can estimate the time of day more exactly.
*/
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int count;
/*
* Here we are in the timer irq handler. We just have irqs locally
* disabled but we don't know if the timer_bh is running on the other
* CPU. We need to avoid to SMP race with it. NOTE: we don' t need
* the irq version of write_lock because as just said we have irq
* locally disabled. -arca
*/
write_lock(&xtime_lock);
if (use_tsc)
{
/*
* It is important that these two operations happen almost at the same time. We do the
* RDTSC stuff first, since it's faster. To avoid any inconsistencies, we need interrupts
* disabled locally.
*/
/*
* Interrupts are just disabled locally since the timer irq
* has the SA_INTERRUPT flag set. -arca
*/
/* read Pentium cycle counter */
rdtscl(last_tsc_low);
spin_lock(&i8253_lock);
outb_p(0x00, 0x43); /* latch the count ASAP */
count = inb_p(0x40); /* read the latched count */
count |= inb(0x40) << 8;
spin_unlock(&i8253_lock);
count = ((LATCH-1) - count) * TICK_SIZE;
delay_at_last_interrupt = (count + LATCH/2) / LATCH;
}
do_timer_interrupt(irq, NULL, regs);
write_unlock(&xtime_lock);
}
對該函數的註釋以下:
(1)因爲函數執行期間要訪問全局時間變量xtime,所以一開就對自旋鎖xtime_lock進行加鎖。
(2)若是內核使用CPU的TSC寄存器(use_tsc變量非0),那麼經過TSC寄存器來計算從時間中斷的產生到timer_interrupt()函數真正在CPU上執行這之間的時間延遲:
l 調用宏rdtscl()將64位的TSC寄存器值中的低32位(LSB)讀到變量last_tsc_low中,以供 do_fast_gettimeoffset()函數計算時間誤差之用。這一步的實質就是將CPU TSC寄存器的值更新到內核對TSC的緩存變量last_tsc_low中。
l 經過讀8254 PIT的通道0的計數器的當前值來計算時間延遲,爲此:首先,對自旋鎖i8253_lock進行加鎖。自旋鎖i8253_lock的做用就是用來串行化對 8254 PIT的讀寫訪問。其次,向8254的控制寄存器(端口0x43)中寫入值0x00,以便對通道0的計數器進行鎖存。最後,經過端口0x40將通道0的計 數器的當前值讀到局部變量count中,並解鎖i8253_lock。
l 顯然,從時間中斷的產生到timer_interrupt()函數真正執行這段時間內,以一共流逝了((LATCH-1)-count)個時鐘週期,所以這個延時長度能夠用以下公式計算:
delay_at_last_interrupt=(((LATCH-1)-count)÷LATCH)﹡TICK_SIZE
顯然,上述公式的結果是個小數,應對其進行四捨五入,爲此,Linux用下述表達式來計算delay_at_last_interrupt變量的值:
(((LATCH-1)-count)*TICK_SIZE+LATCH/2)/LATCH
上述被除數表達式中的LATCH/2就是用來將結果向上圓整成整數的。
(3)在計算出時間延遲後,最後調用函數do_timer_interrupt()執行真正的時鐘服務。
函數do_timer_interrupt()的源碼以下(arch/i386/kernel/time.c):
/*
* timer_interrupt() needs to keep up the real-time clock,
* as well as call the "do_timer()" routine every clocktick
*/
static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
。。。。。。
do_timer(regs);
。。。。。。。
/*
* If we have an externally synchronized Linux clock, then update
* CMOS clock accordingly every ~11 minutes. Set_rtc_mmss() has to be
* called as close as possible to 500 ms before the new second starts.
*/
if ((time_status & STA_UNSYNC) == 0 &&
xtime.tv_sec > last_rtc_update + 660 &&
xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 &&
xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) {
if (set_rtc_mmss(xtime.tv_sec) == 0)
last_rtc_update = xtime.tv_sec;
else
last_rtc_update = xtime.tv_sec - 600; /* do it again in 60 s */
}
……
}
上述代碼中省略了許多與SMP相關的代碼,由於咱們不關心SMP。從上述代碼咱們能夠看出,do_timer_interrupt()函數主要做兩件事:
(1)調用do_timer()函數。
(2)判斷是否須要更新CMOS時鐘(即RTC)中的時間。Linux僅在下列三個條件同時成立時才更新CMOS時鐘:①系統全局時間狀態變量 time_status中沒有設置STA_UNSYNC標誌,也即說明Linux有一個外部同步時鐘。實際上全局時間狀態變量time_status僅在 一種狀況下會被清除STA_SYNC標誌,那就是執行adjtimex()系統調用時(這個syscall與NTP有關)。②自從上次CMOS時鐘更新已 通過去了11分鐘。全局變量last_rtc_update保存着上次更新CMOS時鐘的時間。③因爲RTC存在Update Cycle,所以最好在一秒時間間隔的中間位置500ms左右調用set_rtc_mmss()函數來更新CMOS時鐘。所以Linux規定僅當全局變量 xtime的微秒數tv_usec在500000±(tick/2)微秒範圍範圍以內時,才調用set_rtc_mmss()函數。若是上述條件均成立, 那就調用set_rtc_mmss()將當前時間xtime.tv_sec更新回寫到RTC中。
若是上面是的set_rtc_mmss()函數返回0值,則代表更新成功。因而就將「最近一次RTC更新時間」變量last_rtc_update更新爲 當前時間xtime.tv_sec。若是返回非0值,說明更新失敗,因而就讓last_rtc_update=xtime.tv_sec-600(至關於 last_rtc_update+=60),以便在在60秒以後再次對RTC進行更新。
函數do_timer()實如今kernel/timer.c文件中,其源碼以下:
void do_timer(struct pt_regs *regs)
{
(*(unsigned long *)&jiffies)++;
#ifndef CONFIG_SMP
/* SMP process accounting uses the local APIC timer */
update_process_times(user_mode(regs));
#endif
mark_bh(TIMER_BH);
if (TQ_ACTIVE(tq_timer))
mark_bh(TQUEUE_BH);
}
該函數的核心是完成三個任務:
(1)將表示自系統啓動以來的時鐘滴答計數變量jiffies加1。
(2)調用update_process_times()函數更新當前進程的時間統計信息。注意,該函數的參數原型是「int user_tick」,若是本次時鐘中斷(即時鐘滴答)發生時CPU正處於用戶態下執行,則user_tick參數應該爲1;不然若是本次時鐘中斷髮生時 CPU正處於核心態下執行時,則user_tick參數應改成0。因此這裏咱們以宏user_mode(regs)來做爲 update_process_times()函數的調用參數。該宏定義在include/asm-i386/ptrace.h頭文件中,它根據regs 指針所指向的核心堆棧寄存器結構來判斷CPU進入中斷服務以前是處於用戶態下仍是處於核心態下。以下所示:
#ifdef __KERNEL__
#define user_mode(regs) ((VM_MASK & (regs)->eflags) || (3 & (regs)->xcs))
……
#endif
(3)調用mark_bh()函數激活時鐘中斷的Bottom Half向量TIMER_BH和TQUEUE_BH(注意,TQUEUE_BH僅在任務隊列tq_timer不爲空的狀況下才會被激活)。
至此,內核對時鐘中斷的服務流程宣告結束,下面咱們詳細分析一下update_process_times()函數的實現。