8定時器中斷

走到這裏,大家肯定對Linux的中斷處理有概念了,下面我們通過一個具體的實例,來了解Linux內核處理中斷的全過程,那就是定時器中斷。在詳細分析這個過程之前,我們把Linux時間管理的概念先縷一縷。

 

在當前的80x86體系結構上,內核顯式地與幾種時鐘和定時器電路打交道,其主要分爲了時鐘和定時器兩大類:

- 時鐘電路同時用於跟蹤當前時間和產生精確的時間度量。
- 定時器電路由內核編程,所以它們以固定的、預先定義的頻率發出中斷。

 

1、實時時鐘(RTC)

 

所有的PC都包含一個叫實時時鐘(Renl Time Clock RTC)的時鐘,它是獨立於CPU和所有其他芯片的。即使當PC被切斷電源,RTC還繼續工作,因爲它靠一個小電池或蓄電池供電。CMOS RAM和RTC被集成在一個芯片上。

 

RTC能在IRQ8上發出週期性的中斷,頻率在2~8192 Hz之間。我們可以對RTC進行編程以使當RTC到達某個特定的值時激活IRQ8線,也就是作爲一個鬧鐘來工作。

 

Linux只用RTC來獲取時間和日期,不過,通過對/dev/rtc設備文件進行操作,也允許透程對RTC編程。內核通過0x70和Ox71 I/O端口訪問RTC。系統管理員通過執行Unix系統時鐘程序(直接作用於這兩個I/O端口)可以設置時鐘。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芯片中。所以,RTC時鐘只是個爲後面我們介紹的那些時鐘起一個初始化的作用,僅此而已!

Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h頭文件中分別定義了mc146818 RTC芯片各寄存器的含義以及RTC芯片在i386平臺上的I/O端口操作。而通用的RTC接口則聲明在include/linux/rtc.h頭文件中。

 

2、時間戳計數器(TSC)

 

時間戳計數器與實時時鐘配合使用,用來將時鐘和定時器調整得更精確。所有的80x86微處理器都包含一條CLK輸入引線,它接收外部振盪器的時鐘信號。從Pentium開始,80x86微處理器就都包含一個計數器,它在每次外部振盪器的時鐘信號到來時加1。該計數器是利用64位的時間戳計數器(Time Stamp Counter TSC)寄存器來實現的,可以通過彙編語言指令rdtsc讀這個寄存器

 

爲什麼要出現一個時間戳計數器呢?Linux利用這個寄存器可獲得更精確的時間量,以便計算進程用戶態與內核態的運行時間及等待(睡眠)時間。爲了做到這點,Linux在初始化系統的時候必須確定時鐘信號的頻率。事實上,因爲編譯內核時並不聲明這個頻率,所以同一內核映像可以運行在產生任何時鐘頻率的CPU上,即這個頻率由CPU決定。

 

算出CPU實際頻率的任務是在系統初始化期間完成的。calibrate_tsc()函數通過計算一個大約在5ms的時間間隔內所產生的時鐘信號的個數來算出CPU實際頻率。通過適當地設置可編程間隔定時器(PIT)的一個通道來產生這個時間常量。

 

3、可編程間隔定時器(PIT),重點!

 

可編程間隔定時器(Programmable Interval Timer PIT)的作用類似於微波爐的鬧鐘,即讓用戶意識到烹調的時間間隔已經過了。所不同的是,這個設備不是通過振鈴,而是發出一個特殊的中斷,叫做時鐘中斷(timer interrupt)來通知內核又一個時間間隔過去了。每個IBM兼容PC都至少包含一個PIT,PIT通常是使用0x40~0x43 I/O端口的一個8254 CMOS芯片,該芯片作爲I/O設備與中斷控制器的IRQ0相連,向量號爲32。

 

Linux給PC的PIT進行編程,使它以1000 Hz的頻率向IRQ0發出時鐘中斷,即每1ms產生一次時鐘中斷。這個時間間隔叫做一個節拍(tick),它的長度以納秒爲單位存放在tick_nsec變量中。在PC上,tick_nsec被初始化爲999848ns(產生的時鐘信號頻率大約爲1000.15 Hz),但是如果計算機被外部時鐘同步的話,它的值可能被內核自動調整。

 

時鐘中斷的頻率可以通過編譯內核前對一些參數的設定來滿足於具體硬件體系結構的要求。較慢的機器,其節拍大約爲lOms(每秒產生100次時鐘中斷),而較快的機器的節拍爲大約1ms(每秒產生1000或1024次時鐘中斷)。

 

在Linux的代碼中,有幾個宏產生決定時鐘中斷頻率的常量,對此討論如下:

- HZ產生每秒時鐘中斷的近似個數,也就是時鐘中斷的頻率。在IBM PC上,這個值設置爲1000。
- CLOCK_TICK_RATE產生的值爲1193182,這個值是8254芯片的內部振盪器頻率。
- LATCH產生CLOCK_TICK_RATE和HZ的比值再四捨五入後的整數值。這個值用來對PIT編程。

 

PIT由setup_pit_timer()進行如下初始化:
    spin_lock_irqsave(&i8253_lock, flags);
    outb_p(0x34,0x43);
    udelay(10);
    outb_p(LATCH & 0xff, 0x40);
    udelay(10);
    outb(LATCH >> 8, 0x40);
    spin_unlock_irqrestore(&i8253_lock, flags);

 

outb()C函數等價於outb彙編語言指令:它把第一個操作數拷貝到由第二個操作數指定的I/O端口。outb_p()函數類似於outb(),不過,它會通過一個空操作而產生一個暫停,以避免硬件難以分辨。udelay()宏函數引入了一個更短的延遲。

 

第一條outb_p()語句讓PIT以新的頻率產生中斷。接下來的兩條outb_p()和outb()語句爲設備提供新的中斷頻率。把16位LATCH常量作爲兩個連續的字節發送到設備的8位I/O端口0x40。結果,PIT將以(大約)1000Hz的頻率產生時鐘中斷,也就是說,每1ms產生一次時鐘中斷。

 

4、其他定時器

 

除了PIT,80x86體系還有其他幾個定時器,這裏只簡單提一提:
- CPU本地定時器:CPU本地定時器是一種能夠產生單步中斷或週期性中斷的設備,向量範圍是239 (0xef),比PIT更靈活地編程。
- 高精度事件定時器(HPET):可以通過映射到內存空間的寄存器來對HPET芯片編程的定時器。
- ACPI電源管理定時器:時鐘信號擁有大約爲3.58 MHz的固定頻率,專門針對ACPI電源的定時器。

 

本博文,我們重點討論第三項,即可編程間隔定時器,它是整個Linux內核的心臟,驅動着若干進程的運行。PS,這裏只討論單CPU的計時體系。

 

定時器中斷相關的數據結構

 

定時器對象

 

前面講了那麼多定時器,爲了使用一種統一的方法來處理可能存在的定時器資源,內核使用了“定時器對象”,它是類型爲timer_opts的描述符,該類型由定時器名稱和四個標準的方法組成,如下表所示。

 

字段名

說明

name

標識定時器源的一個字符串

mark_offset

記錄上一個節拍的準確時間,由時鐘中斷處理程序調用

get_offset

返回自上一個節拍開始所經過的時間

monotonic_clock

返回自內核初始化開始所經過的納秒數

delay

等待指定數目的“循環”

 

定時器對象中最重要的方法是mark_offset和get_offset。mark_offset方法由時鐘中斷處理程序調用,並以適當的數據結構記錄每個節拍到來時的準確時間。get_offset方法使用已記錄的值來計算自上一次時鐘中斷(節拍)以來經過的時間(以納秒爲單位)。由於這兩種方法,使得Linux計時體系結構能夠達到子節拍的分辨度,也就是說,內核能夠以比節拍週期更高的精度來測定當前的時間。這種操作被稱作“定時插補(time interpolation)”

 

全局變量cur_timer存放了某個定時器對象的地址,該定時器是系統可利用的定時器資源中“最好的”。最初,cur_timer指向timer_none,這個timer_none是一個虛擬的定時器資源對象,內核在初始化的時候使用它。在內核初始化期間,select_timer()函數設置cur_timer指向適當定時器對象的地址。我們看到,select_timer()將優先選擇HPET(如果可以使用);否則,將選擇ACPI電源管理定時器(如果可以使用);再次之是TSC作爲最後的方案。但是不管前面是什麼方案,select_timer()總是會選擇PIT配合使用,因爲前面的那些時鐘主要是用來維護jiffies_64變量的,我們後面會提及。看下面的表,“定時插補”一列列出了定時器對象的mark_offset方法和get_offset方法所使用的定時器源,“延遲”一列列出了delay方法使用的定時器源。

 

定時器對象名稱

說明

定時插補

延遲

timer_hpet

高精度事件定時器(HPET)

HPET

HPET

timer_pmtmr

ACPI電源管理定時器(ACPI PMT)

ACPI PMT

TSC

timer_tsc

時間戳計數器(TSC)

TSC

TSC

timer_pit

可編程間隔定時器(PIT)

PIT

緊緻循環

timer_none

普通虛擬定時器資源(內核初始化時使用)

(無)

緊緻循環

 

jiffies變量

 

jiffies變量是一個計數器,用來記錄自系統啓動以來產生的總的節拍數。每次時鐘中斷髮生時(每個節拍)它便加1。在80x86體系結構中,jiffies是一個32位的變量,因此每隔大約50天它的值會迴繞(wraparound)到0,這對Linux服務器來說是一個相對較短的時間間隔。不過,由於使用了time_after、time_after_eq、time_before和time_before_eq四個宏(即使發生迴繞它們也能產生正確的值),內核乾淨利索地處理了jiffies變量的溢出。

 

注意,jiffies樁初始化爲Oxfffb6c20,並不是0,它是一個32位的有符號值,正好等於-300000。因此,計數器將會在系統啓動後的5分鐘內處於溢出狀態。這樣做是有目的的,使得那些不對jiffies作溢出檢測的有缺陷的內核代碼在開發階段被及時地發現,從而不再出現在穩定的內核版本中。

 

但是在某些情況下,不管jiffies是否溢出,內核都需要取得自系統啓動以來產生的系統節拍的真實數目。因此,在80x86系統中,jiffies變量通過連接器被換算成一個64位計數器的低32位,這個64位的計數器被稱作jiffies_64。在1ms爲一個節拍的情況下,jiffies_64變量將會在數十億年後才發生迴繞,所以我們可以放心地假定它不會溢出。

 

你可能要問爲什麼在80x86體系結構中jiffies不直接被聲明成64位無符號的長整型數。答案是:在32位的體系結構中不能自動地對64位的變量進行訪問。因此,在每次數行對64位數的讀操作時,需要一些同步機制來保證當兩個32位的計數器(由這兩個32位的計數器組成的64位計數器)的值在被讀取時這個64位的計數器不會被更新,結果是,每個64位的讀操作明顯比32位的讀操作更慢。

 

get_jiffies_64( ) 函數來讀取 jiffies_64 的值並返回該值。

 

xtime變量

xtime變量存放當前時間和日期;它是一個timespec類型的數據結構,該結構有兩個字段:

tv_sec:存放自1970年1月1日(UTC)午夜以來經過的秒數
tv_nsec:存放自上一秒開始經過的納秒數(它的值域範圍在0 - 999999999之間)

 

xtime變量通常是每個節拍更新一次,也就是說,大約每秒更新1000次。用戶程序從xtime變量獲得當前時間和日期。內核也經常引用它,例如,在更新節點時間戳時引用。

 

定時器中斷的上半部分

 

在單處理器系統上,所有與定時有關的活動都是由IRQ線0上的可編程間隔定時器產生的中斷觸發的。還是那句老話,在Linux中,某些活動都儘可能在中斷產生後立即執行,而其餘的活動延遲。

 

初始化階段

 

在內核初始化期間,time_init()函數被調用來建立計時體系結構,它通常執行如下操作

 

1.初始化xtime變量。利用get_cmos_time()函數從實時時鐘上讀取自1970年1月1日(UTC)午夜以來經過的秒數。設置xtime的tv_nsec字段,它將落到秒的範圍內。

 

2.初始化wall_to_monotonic變量。這個變量同xtime一樣是timespec類型,只不過它存放將被加到xtime上的秒數和納秒數,以此來獲得單向(只增)的時間流。其實,外部時鐘的閏秒和同步都有可能突發地改變xtime的tv_sec和tv_nsec字段,這樣使得它們不再是單向遞增的。

 

3.如果內核支持HPET,它將調用hpet_enable()函數來確認ACPI固件是否探測到了該芯片並將它的寄存器映射到了內存地址空間中。如果結果是肯定的,那麼hpet_enable()將對HPET芯片的第一個定時器編程使其以每秒1000次的頻率引發IRQ 0處的中斷。否則,如果不能獲得HPET芯片,內核將使用PIT:該芯片已經被init_IRQ()函數編程,使得它以每秒1000次的頻率引發IRQ 0處的中斷,我們這裏主要就討論這種情況。

 

4. 調用select_timer()來挑選系統中可利用的最好的定時器資源,並設置cur_timer變量指向該定時器資源對應的定時器對象的地址,我們這裏假設使用的是TSC和PIT,上面的第三步未執行。

 

5. 調用setup_irq(0, &irq0)來創建與IRQ0相應的中斷門,IRQ0引腳線連接着系統時鐘中斷源(PIT或HPET)。irq0變量被靜態定義如下:
   
struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL };

 

從現在起,timer_interrupt()函數將會在每個節拍到來時被調用,而中斷被禁止,因爲IRQ0主描述符的狀態字段中的SA_INTERRUPT標誌被置位。

 

定時器中斷總體過程

 

當CPU接收一個定時器(PIT)中斷時,就會馬上跳到common_interrupt彙編程序段中,開始執行相應的中斷處理程序代碼,該代碼的地址存放在IDT的相應門中,那麼針對定時器中斷,這個門的向量就是32號。於是,與其他上下文切換一樣,Linux需要保留當前寄存器的內容以便保存和恢復當前指令。

 

保存寄存器是中斷處理程序做的第一件事情。每個IRQ的中斷處理程序地址存放於interrupt數組中,即IRQ0中斷處理程序的地址開始存在於interrupt[0],然後複製到IDT相應表項的中斷門中。

 

保存寄存器的值以後,棧頂的地址被存放到eax寄存器中,然後中斷處理程序調用do_IRQ()函數。到執行到do_IRQ()的ret指令時(即函數結束時),控制轉到ret_from_intr()(從中斷和異常返回)。

 

do_IRQ()函數不用多說了,因爲前幾篇博文說得很多了,這裏只提一點,我們假設系統式兩片8259A級聯出來的PIC,那麼__do_IRQ()函數獲得自旋鎖後,就調用主IRQ描述符的ack方法,把IRQ號傳給他。ack方法就是mask_and_ack_8259A()函數來應答PIT的中斷,注意禁用這條IRQ線,確保在這個中斷處理程序結束前,CPU不進一步接受這種中斷的出現:handle_IRQ_event(irq, regs, irq_desc[irq].action);這裏的irq_desc[irq].action就是0號中斷服務程序timer_interrupt。之後,__do_IRQ()函數執行irq_desc[irq].handler->end(irq);即調用i8259A_irq_type的end_8259A_irq()函數重新開啓這條IRQ線,也就是0號線。

 

定時器中斷處理程序

 

timer_interrupt()函數是PIT或HPET的中斷服務例程(ISR),它執行以下步驟:

 

1. 在xtime_lock順序鎖上產生一個write_seqlock()來保護與定時相關的內核變量。


2. 執行cur_timer定時器對象的mark_offset方法。正如前面的“時鐘中斷的數據結構”一節解釋的那樣,有四種可能的情況:


a) cur_timer指向timer_hpet對象:這種情況下,HPET芯片作爲時鐘中斷源。mark_offset方法檢查自上一個節拍以來是否丟失時鐘中斷,在這種不太可能發生的情況下,它會相應地更新jiffies_64。接着,該方法記錄下HPET週期計數器的當前值。
b) cur_timer指向timer_pmtmr對象:這種情況下,PIT芯片作爲時鐘中斷源,但是內核使用APIC電源管理定時器以更高的分辨度來測量時間。mark_offset方法檢查自上一個節拍以來是否丟失時鐘中斷,如果丟失則更新jiffies_64。然後,它記錄APIC電源管理定時器計數器的當前值。
c) cur_timer指向timer_tsc對象:這種情況下,PIT芯片作爲時鐘中斷源,但是內核使用時間戳計數器以更高的分辨度來測量時間。mark_offset方法執行與上一種情況相同的操作:檢查自上一個節拍以來是否丟失時鐘中斷,如果丟失則更新jiffies_64。然後,它記錄TSC計數器的當前值。
d) cur_timer指向timer_pit對象(大多數情況下是這樣):這種情況下,PIT芯片作爲時鐘中斷源,除此之外沒有別的定時器電路。mark_offset方法什麼也不做。


3. 調用do_timer_interrupt( )函數,do_timer_interrupt( )函數執行以下操作:


a) 使jiffies_64的值增1。注意,這樣做是安全的,因爲內核控制路徑仍然爲寫操作保持着xtime_lock順序鎖。
b) 調用update_times()函數來更新系統日期和時間,並計算當前系統負載。
c) 調用update_process_times()函數爲本地CPU上運行鏈表的進程執行幾個與定時相關的計數操作,參見博文“scheduler_tick函數”。
d) 調用profile_tick()函數。
e) 如果使用外部時鐘來同步系統時鐘(以前已發出過adjtimex()系統調用),則每隔660秒(每隔11分鐘)調用一次set_rtc_mmss()函數來調整實時時鐘。這個特性用來幫助網絡中的系統同步它們的時鐘。


4. 調用write_sequnlock()釋放xtime_lock順序鎖。


5. 返回值1,報告中斷已經被有效地處理了。

 

這裏我們再進一步深入分析一下do_timer_interrupt函數。在這裏,我們這裏暫不討論SMP結構中採用APIC時的特殊處理(CONFIG_X86_IO_APIC),也不討論SGI工作站和PS/2的“Micro chanel”的特殊情況,此外我們也不關心時鐘的精度(time_status變量的條件語句)。所以我們重點關注定時中斷的核心步驟:update_times和update_process_times,其通過函數do_timer_interrupt_hook被觸發。

 

update_times函數

 

先來看update_times():

static inline void update_times(void)
{
    unsigned long ticks;

    ticks = jiffies - wall_jiffies;
     if (ticks) {
          wall_jiffies += ticks;
          update_wall_time(ticks);
     }
     calc_load(ticks);
}

 

這裏做了兩件事。第一件是update_wall_time(),目的是處理所謂“實時時鐘”,或者說“牆上時間”xtime中的數值,包括計數,進位,以及爲精度目的而做的校正。我們暫時不深入了。這裏的wall_jiffies也像jiffies一樣是個全局量,它代表着與當前xtime中的數值相對應的jiffies值,表示“掛鐘”當前的讀數已經校準到了時軸上的哪一點。

 

第二件事是calc_load(),目的是計算和積累關於CPU負荷統計信息。內核每隔5秒鐘計算、累積和更新一次系統在過去15分鐘、10分鐘以及1分鐘內平均有多少個進程處於可執行狀態,作爲衡量系統負荷輕重的指標。由於涉及的主要是數值計算,我們就不深入進去了。

 

profile_tick函數

 

do_timer_interrupt函數調用profile_tick()函數,內核進行性能分析。通過該函數在每次系統時鐘中斷時的採樣,並對採樣結果進行分析,可以獲知系統的性能瓶頸在什麼地方。

 

update_process_times函數(重點)

 

函數update_process_times()則對當前被中斷進程進行記賬,減小進程剩餘可用的時間片。然後激活其他內核模塊的處理函數,如同步機制RCU的處理函數、內核定時器的處理函數。首先,do_timer_interrupt_hook()調用該函數時,傳遞進來一個實際參數user_mode(regs),該參數使用函數 user_mode()來判斷被中斷時系統處於用戶態還是內核態。該函數根據被打斷的上下文所使用的指令段選擇子寄存器cs的CPL的字段是否爲3,如果爲3則返回1,表示是用戶態;否則返回0,表示爲內核態。

 

隨後通過宏定義current、smp_processor_id()獲得當前進程描述符指針和當前處理器編號,並將其分別保存到指針變量p和整型cpu中。

然後,根據被中斷進程運行於用戶態還是系統態分別調用account_user_time()、account_system_time()對被中斷進程進行記賬。它們分別將進程描述符中用戶態時間字段utime、內核態時間字段stime的值加上jiffies_to_cputime(1),表示進程在用戶態或者內核又運行了一個系統時鐘滴答。然後更新處理器歷史統計信息。其中jiffies_to_cputime()是一個宏定義,該宏定義用於將一個時鐘中斷轉換爲處理器時間。

 

之後,調用函數run_local_timers()設置時鐘中斷處理的下半部處理標記、激活時鐘中斷處理的下半部。該下半部負責維護、更新內核定時器鏈表,對於超時的內核定時器執行相應的超時處理函數,並將超時的定時器移出內核定時器鏈表。其中函數 run_local_timers()在文件src/kernel/timer.c中定義如下:

void run_local_timers(void)
{
       raise_softirq(TIMER_SOFTIRQ);
}

 

定時器中斷的下半部分

 

當執行完run_local_timers()函數以後,整個定時器中斷進入了下半部分。好了,我們又來回憶下半部分的基礎知識。定時器中斷的下半部屬於軟中斷,沒有涉及tasklet,前面講過,軟中斷的數據結果如圖所示:

 

定時器軟中斷

 

初始化的時候有個init_timers 函數 ,該函數執行一條命令:open_softirq(TIMER_SOFTIRQ,run_timer_softirq, NULL);,也就是優先級最高的軟中斷TIMER_SOFTIRQ對應的處理函數爲run_timer_softirq,即把SOFTIRQ_VEC[0]的action指向run_timer_softirq,data當然就是空的NULL了。所以,當執行raise_softirq(TIMER_SOFTIRQ); 函數時,就會告訴首先設置每個CPU的irq_cpustat_t數據結構的 __softirq_pending字段對應定時器軟中斷的那個位,這個操作被稱爲“掛起軟中斷”,即掛起0號軟中斷(定時器優先級最高,爲0),實現代碼如下:

 

local_softirq_pending() |= 1UL << (nr); //對於定時器軟中斷來說,這個nr就是0.

 

注意,前一篇博文說過了,內核使用宏local_softirq_pending(),獲得本地CPU的軟中斷位掩碼__softirq_pending。

 

raise_softirq(TIMER_SOFTIRQ)還要調用in_interrupt()函數來做一個判斷,判斷兩種情況,一種是對應的raise_softirq被其他中斷上下文調用了,一種是當前系統禁用了軟中斷,注意!是軟中斷,不是eflags寄存器的IF位。

 

繼續往下走,raise_softirq(TIMER_SOFTIRQ)調用wakeup_softirqd()以喚醒本地CPU的ksoftirqd內核線程,該線程週期性地調用do_softirq( );只要local_softirq_pending()返回不爲0,就調用,因爲說明有某個或多個軟中斷是掛着的,待處理的。

 

當然,我們這裏肯定不爲0,所以調用do_softirq,執行local_irq_save以保存IF標誌的狀態值,並禁用本地CPU上的中斷

 

然後調用__do_softirq()函數,該函數主要執行這麼一個循環:

 

pending =local_softirq_pending;

 do {
    if (pending & 1) {
        h->action(h);
        rcu_bh_qsctr_inc(cpu);
   }
   h++;
   pending >>= 1;
 } while (pending);

 

__do_softirq()函數讀取本地CPU的軟中斷掩碼並執行與每個設置位相關的可延遲函數。由於正在執行一個軟中斷函數時可能出現新掛起的軟中斷,所以爲了保證可延遲函數的低延遲性,__do_softirq()一直運行到執行完所有掛起的軟中斷。

 

最後回到do_softirq()函數中,執行local_irq_restore以恢之前保存的IF標誌(表示本地是關中斷還是開中斷)的狀態值並返回。

 

講到這裏,我們才進入軟中斷的真正處理函數h->action(h);,也就是跳到前邊提到的run_timer_softirq()。該函數作爲後半部在合適的時刻開始運行負責對內核定時器鏈表中的定時器進行處理。接下來我們從定時器中斷下半部的入口函數run_timer_softirq()開始做一個簡單的分析,看一看定時器中斷下半部都具體完成了哪些工作。

 

該函數是定時器中斷處理的下半部,負責判斷當前處理器上是否有定時器超時,並在有定時器超時的情況下進行超時處理。

 

run_timer_softirq()首先聲明並初始化指針變量base指向每處理器變量tvec_bases的本地拷貝的地址。 隨後使用宏定義time_after_eq()判斷當前處理器上的待處理內核定時器鏈表中是否有超時的內核定時器,如果有,調用函數__run_timers()進行處理。

 

__run_timers()函數負責調用超時內核定時器對應的超時處理函數,完成超時處理。在必要時對內核定時器核心數據結構tvec_base_t進行更新和維護,以保證能夠快速訪問超時定時器所在的鏈表。

 

回到update_process_times函數當中,當下半部主要函數run_timer_softirq()執行完畢後,有一個重要的收尾工作,也就是執行scheduler_tick()。

 

當每次時鐘節拍到來時,scheduler_tick()負責執行以下步驟:
1.  把轉換爲納秒的TSC的當前值存入本地運行隊列的timestamp_last_tick字段,即記錄最近一次定時器中斷的時間戳 。這個時間戳是從函數sched_clock()獲得的。
2.  檢查當前進程是否是本地CPU的swapper進程,如果是,執行下面的子步驟:


a)  如果本地運行隊列除了swapper進程外,還包括另外一個可運行的進程,就設置當前進程的TIF_NEED_RESCHED字段,以強迫進行重新調度。就像我們稍後在講schedule函數所看到的,如果內核支持超線程技術,那麼,只要一個邏輯CPU運行隊列中的所有進程都有比另一個邏輯CPU(兩個邏輯 CPU對應同一個物理CPU)上已經在執行的進程有低得多的優先級,前一個邏輯CPU就可能空閒,即使它的運行隊列中有可運行的進程。
b)  跳轉到第7步(沒有必要更新swapper進程的時間片計數器)


3.  檢查current->array是否指向本地運行隊列的活動鏈表。如果不是,說明進程已經過期但還沒有被替換:設置TIF_NEED_RESCHED字段,以強迫進行重新調度並跳轉到第7步。
4.  獲得this_rq()->lock自旋鎖。
5.  遞減當前進程的時間片計數器,並檢查是否已經用完時間片。由於進程的調度類型不同,函數所執行的這一步操作也有很大的差別,我們馬上將會討論它。
6.  釋放this_rq()->lock自旋鎖。
7.  調用rebalance_tick()函數,該函數應該保證不同CPU的運行隊列包含數量基本相同的可運行進程。

 

第5步中,我們分情況請進行說明:

 

更新實時進程的時間片

 

如果當前進程是先進先出(FIFO)的實時進程,函數scheduler_tick()什麼都不做。實際上在這種情況下,current所表示的當前進程想佔用CPU多久就佔用多久,而且不可能比其他優先級低或其他優先級相等的進程所搶佔,因此,維持當前進程的最新時間片計數器是沒有意義的。

 

如果current表示基於時間片輪轉的實時進程,scheduler_tick()就遞減它的時間片計數器並檢查時間片是否被用完:

 

if (current->policy == SCHED_RR && !--current->time_slice) {
    current->time_slice = task_timeslice(current);
    current->first_time_slice = 0;
    set_tsk_need_resched(current);
    list_del(&current->run_list);
    list_add_tail(&current->run_list,
                         this_rq( )->active->queue+current->prio);
}

 

如果函數確定時間片確實用完了,就執行一系列操作以達到搶佔當前進程的目的,如果必要的話,就儘快搶佔:

 

第一步操作包括調用task_timeslice()來重填進程的時間片計數器。該函數檢查進程的靜態優先級,並根據前面“普通進程的調度”公式返回相應的基本時間片。此外,current的first_time_slice字段被清零:該標誌被fork系統調用例程中的 copy_process()設置,並在進程的第一個時間片剛用完時立刻清零。

 

第二步,scheduler_tick()函數調用函數set_tsk_need_resched()設置進程的 TIF_NEED_RESCHED標誌。該標誌強制調用schedule()函數,以便current指向的進程能被另外一個有相同優先級或更高優先級的實時進程所取代。

 

scheduler_tick()的最後一步操作包括把進程描述符移到與當前進程優先級相應的運行隊列活動鏈表的尾部。把current指向的進程放到鏈表的尾部,可以保證每個優先級與它相同的可運行實時進程獲得CPU時間片以前,它不會再次被選擇來執行。這是基於時間片輪轉的調度策略。進程描述符的移動是通過兩個步驟完成的:先調用list_del()把進程從運行隊列的活動鏈表中刪除,然後調用list_add_tail()把進程重新插入到同一個活動鏈表的尾部。

 

更新普通進程的時間片

 

如果當前進程是普通進程,函數scheduler_tick()執行下列操作:
1.   遞減時間片計數器(current->time_slice)。
2.   檢查時間片計數器。如果時間片用完,函數執行下列操作:


a)   調用dequeue_task()從可運行進程的this_rq()->active集合中刪除current指向的進程。
b)   調用set_tsk_need_resched( )設置TIF_NEED_RESCHED標誌。
c)   更新current指向的進程的動態優先級:current->prio = effective_prio(current);。函數effective_prio()讀current的static_prio和 sleep_avg字段,並根據前面的公式計算出進程的動態優先級。
d)   重填進程的時間片:
current->time_slice = task_timeslice(current);
current->first_time_slice = 0;
e)   如果本地運行隊列數據結構中的expired_timestamp字段等於0(即過期進程集合爲空),就把當前時鐘節拍值賦給expired_timestamp:

if (!this_rq( )->expired_timestamp)    
this_rq( )->expired_timestamp = jiffies;
f)   把當前進程插入活動進程集合或過期進程集合:

if (!TASK_INTERACTIVE(current) || EXPIRED_STARVING(this_rq( )) {
    enqueue_task(current, this_rq( )->expired);
    if (current->static_prio < this_rq( )->best_expired_prio)
        this_rq( )->best_expired_prio = current->static_prio;
    } else
        enqueue_task(current, this_rq( )->active);


如果用前面列出的公式(3)識別出進程是一個交互式進程,TASK_INTERACTIVE宏就產生1。宏EXPIRED_STARVING檢查運行隊列中的第一個過期進程的等待時間是否已經超過1000個時鐘節拍乘以運行隊列中的可運行進程數加1,如果是,宏產生1。如果當前進程的靜態優先級大於一個過期進程的靜態優先級,EXPIRED_STARVING宏也產生1。

 

3.   否則,如果時間片沒有用完(current->time_slice不等於0),檢查當前進程的剩餘時間片是否太長:

if (TASK_INTERACTIVE(p) && !((task_timeslice(p) -
        p->time_slice) % TIMESLICE_GRANULARITY(p)) &&
        (p->time_slice >= TIMESLICE_GRANULARITY(p)) &&
        (p->array == rq->active)) {
    list_del(&current->run_list);
    list_add_tail(&current->run_list,
        this_rq( )->active->queue+current->prio);
    set_tsk_need_resched(p);
}
宏TIMESLICE_GRANULARITY產生兩個數的乘積給當前進程的bonus,其中一個數爲系統中的CPU數量,另一個爲成比例的常量。基本上,具有高靜態優先級的交互式進程,其時間片被分成大小爲TIMESLICE_GRANULARITY的幾個片段,以使這些進程不會獨佔CPU。



轉載自http://blog.csdn.net/yunsongice


發佈了0 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章