歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> 關於Unix

第七章 Linux內核的時鐘中斷(中)

7.3 Linux對時間的表示 7.4 時鐘中斷的驅動 7.3 Linux對時間的表示 通常,操作系統可以使用三種方法來表示系統的當前時間與日期:①最簡單的一種方法就是直接用一個64位的計數器來對時鐘滴答進行計數。②第二種方法就是用一個32位計數器來對秒進行計數,同 7.3 Linux對時間的表示
7.4 時鐘中斷的驅動

7.3 Linux對時間的表示
通常,操作系統可以使用三種方法來表示系統的當前時間與日期:①最簡單的一種方法就是直接用一個64位的計數器來對時鐘滴答進行計數。②第二種方法就是用一個32位計數器來對秒進行計數,同時還用一個32位的輔助計數器對時鐘滴答計數,之子累積到一秒為止。因為232超過136年,因此這種方法直至22世紀都可以讓系統工作得很好。③第三種方法也是按時鐘滴答進行計數,但是是相對於系統啟動以來的滴答次數,而不是相對於相對於某個確定的外部時刻;當讀外部後備時鐘(如RTC)或用戶輸入實際時間時,根據當前的滴答次數計算系統當前時間。
UNIX類操作系統通常都采用第三種方法來維護系統的時間與日期。

7.3.1 基本概念
首先,有必要明確一些Linux內核時鐘驅動中的基本概念。
(1)時鐘周期(clock cycle)的頻率:8253/8254PIT的本質就是對由晶體振蕩器產生的時鐘周期進行計數,晶體振蕩器在1秒時間內產生的時鐘脈沖個數就是時鐘周期的頻率。Linux用宏CLOCK_TICK_RATE來表示8254PIT的輸入時鐘脈沖的頻率(在PC機中這個值通常是1193180HZ),該宏定義在include/asm-i386/timex.h頭文件中:
#define CLOCK_TICK_RATE1193180 /* Underlying HZ */
(2)時鐘滴答(clock tick):我們知道,當PIT通道0的計數器減到0值時,它就在IRQ0上產生一次時鐘中斷,也即一次時鐘滴答。PIT通道0的計數器的初始值決定了要過多少時鐘周期才產生一次時鐘中斷,因此也就決定了一次時鐘滴答的時間間隔長度。
(3)時鐘滴答的頻率(HZ):也即1秒時間內PIT所產生的時鐘滴答次數。類似地,這個值也是由PIT通道0的計數器初值決定的(反過來說,確定了時鐘滴答的頻率值後也就可以確定8254PIT通道0的計數器初值)。Linux內核用宏HZ來表示時鐘滴答的頻率,而且在不同的平台上HZ有不同的定義值。對於ALPHA和IA62平台HZ的值是1024,對於SPARC、MIPS、ARM和i386等平台HZ的值都是100。該宏在i386平台上的定義如下(include/asm-i386/param.h):
#ifndef HZ
#define HZ 100
#endif
根據HZ的值,我們也可以知道一次時鐘滴答的具體時間間隔應該是(1000ms/HZ)=10ms。
(4)時鐘滴答的時間間隔:Linux用全局變量tick來表示時鐘滴答的時間間隔長度,該變量定義在kernel/timer.c文件中,如下:
long tick = (1000000 + HZ/2) / HZ;/* timer interrupt period */
tick變量的單位是微妙(μs),由於在不同平台上宏HZ的值會有所不同,因此方程式tick=1000000÷HZ的結果可能會是個小數,因此將其進行四捨五入成一個整數,所以Linux將tick定義成(1000000+HZ/2)/HZ,其中被除數表達式中的HZ/2的作用就是用來將tick值向上圓整成一個整型數。
另外,Linux還用宏TICK_SIZE來作為tick變量的引用別名(alias),其定義如下(arch/i386/kernel/time.c):
#define TICK_SIZE tick
(5)宏LATCH:Linux用宏LATCH來定義要寫到PIT通道0的計數器中的值,它表示PIT將沒隔多少個時鐘周期產生一次時鐘中斷。顯然LATCH應該由下列公式計算:
LATCH=(1秒之內的時鐘周期個數)÷(1秒之內的時鐘中斷次數)=(CLOCK_TICK_RATE)÷(HZ)
類似地,上述公式的結果可能會是個小數,應該對其進行四捨五入。所以,Linux將LATCH定義為(include/linux/timex.h):
/* LATCH is used in the interval timer and ftape setup. */
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ)/* For divider */
類似地,被除數表達式中的HZ/2也是用來將LATCH向上圓整成一個整數。

7.3.2 表示系統當前時間的內核數據結構
作為一種UNIX類操作系統,Linux內核顯然采用本節一開始所述的第三種方法來表示系統的當前時間。Linux內核在表示系統當前時間時用到了三個重要的數據結構:
①全局變量jiffies:這是一個32位的無符號整數,用來表示自內核上一次啟動以來的時鐘滴答次數。每發生一次時鐘滴答,內核的時鐘中斷處理函數timer_interrupt()都要將該全局變量jiffies加1。該變量定義在kernel/timer.c源文件中,如下所示:
unsigned long volatile jiffies;
C語言限定符volatile表示jiffies是一個易該變的變量,因此編譯器將使對該變量的訪問從不通過CPU內部cache來進行。
②全局變量xtime:它是一個timeval結構類型的變量,用來表示當前時間距UNIX時間基准1970-01-0100:00:00的相對秒數值。結構timeval是Linux內核表示時間的一種格式(Linux內核對時間的表示有多種格式,每種格式都有不同的時間精度),其時間精度是微秒。該結構是內核表示時間時最常用的一種格式,它定義在頭文件include/linux/time.h中,如下所示:
struct timeval {
time_ttv_sec;/* seconds */
suseconds_ttv_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的更新並不是一項緊迫的任務,所以這一工作通常被延遲到時鐘中斷的底半部分(bottomhalf)中來進行。由於bottomhalf的執行時間帶有不確定性,因此為了記住內核上一次更新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 {
inttz_minuteswest;/* minutes west of Greenwich */
inttz_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寄存器的宏操作
x86CPU的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()函數對計數器、時間中斷的BottomHalf進行初始化。(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)設置8254PIT的控制寄存器(端口0x43)的值為“01100100”,也即選擇通道0、先讀寫LSB再讀寫MSB、工作模式2、二進制存儲格式。(2)將宏LATCH的值寫入通道0的計數器中(端口0x40),注意要先寫LATCH的LSB,再寫LATCH的高字節。其源碼如下所示(arch/i386/kernel/i8259.c):
void __init init_IRQ(void)
{
……
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43);/* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40);/* LSB */
outb(LATCH >> 8 , 0x40);/* MSB */
……
}

(2)sched_init()對定時器機制和時鐘中斷的Bottom Half的初始化
函數sched_init()中與時間相關的初始化過程主要有兩步:(1)調用init_timervecs()函數初始化內核定時器機制;(2)調用init_bh()函數將BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所對應的BH函數分別設置成timer_bh()、tqueue_bh()和immediate_bh()函數。如下所示(kernel/sched.c):
void __init sched_init(void)
{
……
init_timervecs();

init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
……
}

(3)time_init()函數對內核時鐘中斷機制的初始化
前面兩個函數所進行的初始化步驟都是為時間中斷機制做好准備而已。在執行完init_IRQ()函數和sched_init()函數後,CPU已經可以為IRQ0上的時鐘中斷進行服務了,因為IRQ0所對應的中斷門已經被設置好指向中斷服務函數IRQ0x20_interrupt()。但是由於此時中斷向量0x20的中斷向量描述符irq_desc[0]還是處於初始狀態(其status成員的值為IRQ_DISABLED),並未掛接任何具體的中斷服務描述符,因此這時CPU對IRQ0的中斷服務並沒有任何具體意義,而只是按照規定的流程空跑一趟。但是當CPU執行完time_init()函數後,情形就大不一樣了。
函數time_init()主要做三件事:(1)從RTC中獲取內核啟動時的時間與日期;(2)在CPU有TSC的情況下校准TSC,以便為後面使用TSC做好准備;(3)在IRQ0的中斷請求描述符中掛接具體的中斷服務描述符。其源碼如下所示(arch/i386/kernel/time.c):
void __init time_init(void)
{
extern int x86_udelay_tsc;

xtime.tv_sec = get_cmos_time();
xtime.tv_usec = 0;

/*
* If we have APM enabled or the CPU clock speed is variable
* (CPU stops clock on HLT or slows clock to save power)
* then the TSC timestamps may diverge by up to 1 jiffy from
* 'real time' but nothing will break.
* The most frequent case is that the CPU is "woken" from a halt
* state by the timer interrupt itself, so we get 0 error. In the
* rare cases where a driver would "wake" the CPU and request a
* timestamp, the maximum error is < 1 jiffy. But timestamps are
* still perfectly ordered.
* Note that the TSC counter will be reset if APM suspends
* to disk; this won't break the kernel, though, 'cuz we're
* smart. See arch/i386/kernel/apm.c.
*/
/*
*Firstly we have to do a CPU check for chips with
* a potentially buggy TSC. At this point we haven't run
*the ident/bugs checks so we must run this hook as it
*may turn off the TSC flag.
*
*NOTE: this doesnt yet handle SMP 486 machines where only
*some CPU's have a TSC. Thats never worked and nobody has
*moaned if you have the only one in the world - you fix it!
*/

dodgy_tsc();

if (cpu_has_tsc) {
unsigned long tsc_quotient = calibrate_tsc();
if (tsc_quotient) {
fast_gettimeoffset_quotient = tsc_quotient;
use_tsc = 1;
/*
*We could be more selective here I suspect
*and just enable this for the next intel chips ?
*/
x86_udelay_tsc = 1;
#ifndef do_gettimeoffset
do_gettimeoffset = do_fast_gettimeoffset;
#endif
do_get_fast_time = do_gettimeofday;

/* report CPU clock rate in Hz.
* The formula is (10^6 * 2^32) / (2^32 * 1 / (clocks/us)) =
* clock/second. Our precision is about 100 ppm.
*/
{unsigned long eax=0, edx=1000;
__asm__("divl %2"
:"=a" (cpu_khz), "=d" (edx)
:"r" (tsc_quotient),
"0" (eax), "1" (edx));
printk("Detected %lu.%03lu MHz processor.\n", cpu_khz / 1000, cpu_khz % 1000);
}
}
}

#ifdef CONFIG_VISWS
printk("Starting Cobalt Timer system clock\n");

/* Set the countdown value */
co_cpu_write(CO_CPU_TIMEVAL, CO_TIME_HZ/HZ);

/* Start the timer */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) | CO_CTRL_TIMERUN);

/* Enable (unmask) the timer interrupt */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) & ~CO_CTRL_TIMEMASK);

/* Wire cpu IDT entry to s/w handler (and Cobalt APIC to IDT) */
setup_irq(CO_IRQ_TIMER, &irq0);
#else
setup_irq(0, &irq0);
#endif
}
對該函數的注解如下:
(1)調用函數get_cmos_time()從RTC中得到系統啟動時的時間與日期,它返回的是當前時間相對於1970-01-0100:00:00這個UNIX時間基准的秒數值。因此這個秒數值就被保存在系統全局變量xtime的tv_sec成員中。而xtime的另一個成員tv_usec則被初始化為0。
(2)通過dodgy_tsc()函數檢測CPU是否存在時間戳記數器BUG(I know nothing about it:-)
(3)通過宏cpu_has_tsc來確定系統中CPU是否存在TSC計數器。如果存在TSC,那麼內核就可以用TSC來獲得更為精確的時間。為了能夠用TSC來修正內核時間。這裡必須作一些初始化工作:①調用calibrate_tsc()來確定TSC的每一次計數真正代表多長的時間間隔(單位為us),也即一個時鐘周期的真正時間間隔長度。②將calibrate_tsc()函數所返回的值保存在全局變量fast_gettimeoffset_quotient中,該變量被用來快速地計算時間偏差;同時還將另一個全局變量use_tsc設置為1,表示內核可以使用TSC。這兩個變量都定義在arch/i386/kernel/time.c文件中,如下:
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
……
static int use_tsc;
③接下來,將系統全局變量x86_udelay_tsc設置為1,表示可以通過TSC來實現微妙級的精確延時。該變量定義在arch/i386/lib/delay.c文件中。④將函數指針do_gettimeoffset強制性地指向函數do_fast_gettimeoffset()(與之對應的是do_slow_gettimeoffset()函數),從而使內核在計算時間偏差時可以用TSC這種快速的方法來進行。⑤將函數指針do_get_fast_time指向函數do_gettimeofday(),從而可以讓其他內核模塊通過do_gettimeofday()函數來獲得更精准的當前時間。⑥計算並報告根據TSC所算得的CPU時鐘頻率。
(4)不考慮CONFIG_VISWS的情況,因此time_init()的最後一個步驟就是調用setup_irq()函數來為IRQ0掛接具體的中斷服務描述符irq0。全局變量irq0是時鐘中斷請求的中斷服務描述符,其定義如下(arch/i386/kernel/time.c):
static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
顯然,函數timer_interrupt()將成為時鐘中斷的服務程序(ISR),而SA_INTERRUPT標志也指定了timer_interrupt()函數將是在CPU關中斷的條件下執行的。結構irq0中的next指針被設置為NULL,因此IRQ0所對應的中斷服務隊列中只有irq0這唯一的一個元素,且IRQ0不允許中斷共享。

7.4.2 時鐘中斷服務例程timer_interrupt()
中斷服務描述符irq0一旦被鉤掛到IRQ0的中斷服務隊列中去後,Linux內核就可以通過irq0->handler函數指針所指向的timer_interrupt()函數對時鐘中斷請求進行真正的服務,而不是向前面所說的那樣只是讓CPU“空跑”一趟。此時,Linux內核可以說是真正的“跳動”起來了。
在本節一開始所述的對時鐘中斷驅動的5項要求中,通常只有第一項(即timekeeping)是最為迫切的,因此必須在時鐘中斷服務例程中完成。而其余的幾個要求可以稍緩,因此可以放在時鐘中斷的BottomHalf中去執行。這樣,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()函數計算時間偏差之用。這一步的實質就是將CPUTSC寄存器的值更新到內核對TSC的緩存變量last_tsc_low中。
l 通過讀8254PIT的通道0的計數器的當前值來計算時間延遲,為此:首先,對自旋鎖i8253_lock進行加鎖。自旋鎖i8253_lock的作用就是用來串行化對8254PIT的讀寫訪問。其次,向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 aclearcase/" target="_blank" >ccordingly 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存在UpdateCycle,因此最好在一秒時間間隔的中間位置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()函數更新當前進程的時間統計信息。注意,該函數的參數原型是“intuser_tick”,如果本次時鐘中斷(即時鐘滴答)發生時CPU正處於用戶態下執行,則user_tick參數應該為1;否則如果本次時鐘中斷發生時CPU正處於核心態下執行時,則user_tick參數應改為0。所以這裡我們以宏user_mode(regs)來作為update_process_times()函數的調用參數。該宏定義在include/asm-i386/ptrace.h頭文件中,它根據regs指針所指向的核心堆棧寄存器結構來判斷CPU進入中斷服務之前是處於用戶態下還是處於核心態下。如下所示:
#ifdef __KERNEL__
#define user_mode(regs) ((VM_MASK & (regs)->eflags) || (3 & (regs)->xcs))
……
#endif
(3)調用mark_bh()函數激活時鐘中斷的Bottom Half向量TIMER_BH和TQUEUE_BH(注意,TQUEUE_BH僅在任務隊列tq_timer不為空的情況下才會被激活)。

至此,內核對時鐘中斷的服務流程宣告結束,下面我們詳細分析一下update_process_times()函數的實現。

7.4.3 更新時間記帳信息——CPU分時的實現
函數update_process_times()被用來在發生時鐘中斷時更新當前進程以及內核中與時間相關的統計信息,並根據這些信息作出相應的動作,比如:重新進行調度,向當前進程發出信號等。該函數僅有一個參數user_tick,取值為1或0,其含義在前面已經敘述過。
該函數的源代碼如下(kernel/timer.c):
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;

update_one_process(p, user_tick, system, cpu);
if (p->pid) {
if (--p->counter <= 0) {
p->counter = 0;
p->need_resched = 1;
}
if (p->nice > 0)
kstat.per_cpu_nice[cpu] += user_tick;
else
kstat.per_cpu_user[cpu] += user_tick;
kstat.per_cpu_system[cpu] += system;
} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)
kstat.per_cpu_system[cpu] += system;
}
(1)首先,用smp_processor_id()宏得到當前進程的CPU ID。
(2)然後,讓局部變量system=user_tick^1,表示當發生時鐘中斷時CPU是否正處於核心態下。因此,如果user_tick=1,則system=0;如果user_tick=0,則system=1。
(3)調用update_one_process()函數來更新當前進程的task_struct結構中的所有與時間相關的統計信息以及成員變量。該函數還會視需要向當前進程發送相應的信號(signal)。
(4)如果當前進程的PID非0,則執行下列步驟來決定是否重新進行調度,並更新內核時間統計信息:
l將當前進程的可運行時間片長度(由task_struct結構中的counter成員表示,其單位是時鐘滴答次數)減1。如果減到0值,則說明當前進程已經用完了系統分配給它的的運行時間片,因此必須重新進行調度。於是將當前進程的task_struct結構中的need_resched成員變量設置為1,表示需要重新執行調度。
l如果當前進程的task_struct結構中的nice成員值大於0,那麼將內核全局統計信息變量kstat中的per_cpu_nice[cpu]值將上user_tick。否則就將user_tick值加到內核全局統計信息變量kstat中的per_cpu_user[cpu]成員上。
l將system變量值加到內核全局統計信息kstat.per_cpu_system[cpu]上。
(5)否則,就判斷當前CPU在服務時鐘中斷前是否處於softirq軟中斷服務的執行中,或則正在服務一次低優先級別的硬件中斷中。如果是這樣的話,則將system變量的值加到內核全局統計信息kstat.per_cpu.system[cpu]上。

lupdate_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軟件定時器更新時間間隔。

ldo_process_times()函數
函數do_process_times()將更新指定進程的總時間統計信息。每個進程task_struct結構中都有一個成員times,它是一個tms結構類型(include/linux/times.h):
struct tms {
clock_t tms_utime; /* 本進程在用戶台下的執行時間總和 */
clock_t tms_stime; /* 本進程在核心態下的執行時間總和 */
clock_t tms_cutime; /* 所有子進程在用戶態下的執行時間總和 */
clock_t tms_cstime; /* 所有子進程在核心態下的執行時間總和 */
};
上述結構的所有成員都以時鐘滴答次數為單位。
函數do_process_times()的源碼如下(kernel/timer.c):
static inline void do_process_times(struct task_struct *p,
unsigned long user, unsigned long system)
{
unsigned long psecs;

psecs = (p->times.tms_utime += user);
psecs += (p->times.tms_stime += system);
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_cur) {
/* Send SIGXCPU every second.. */
if (!(psecs % HZ))
send_sig(SIGXCPU, p, 1);
/* and SIGKILL when we go over max.. */
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_max)
send_sig(SIGKILL, p, 1);
}
}
注釋如下:
(1)根據參數user更新指定進程task_struct結構中的times.tms_utime值。根據參數system更新指定進程task_struct結構中的times.tms_stime值。
(2)將更新後的times.tms_utime值與times.tms_stime值的和保存到局部變量psecs中,因此psecs就表示了指定進程p到目前為止已經運行的總時間長度(以時鐘滴答次數計)。如果這一總運行時間長超過進程P的資源限額,那就每隔1秒給進程發送一個信號SIGXCPU;如果運行時間長度超過了進程資源限額的最大值,那就發送一個SIGKILL信號殺死該進程。

ldo_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;
}
}

ldo_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;
}
}

Copyright © Linux教程網 All Rights Reserved