1.異常和中斷上下文
一個中斷處理常式既可以搶佔其他中斷處理常式,也可以搶佔例外處理常式,相反,例外處理常式從不搶佔中斷處理常式。在核心態能觸發的唯一異常就是缺頁異常。但是,中斷處理常式從不執行可以導致缺頁(因此意味著進程切換)的操作。
補充:此處的異常是指除中斷irq以外的異常,它們處於進程上下文,而不是中斷上下文。
2.關於核心搶佔
核心搶佔是Linux 2.6中一個重要的概念。我們說:如果進程正執行核心功能時,即它在核心態運行時,允許發生核心切換(被替換的進程是正執行核心功能的進程),這個核心就是搶佔的。遺憾的是,在Linux中(在所有其他的作業系統中也一樣),情況要複雜得多:
第一:無論在搶佔核心還是非搶佔核心中,運行在核心態的進程都可以自動放棄CPU,比如,其原因可能是,進程由於等待資源而不得不轉入睡眠狀態。ULK-3中把這種進程切換稱為計劃性進程切換。但是,搶佔式核心在響應引起進程切換的非同步事件(例如喚醒高優先權進程的中斷處理常式)的方式上與非搶佔的核心是有差別的,我們將把這種進程切換稱做強制性進程切換。
第二:所有的進程切換都由宏switch_to所代表的彙編程式碼片段來完成,這一點在進程管理專題也描述得很清楚。在搶佔核心和非搶佔核心中,當進程執行完某些具有核心功能的線程,而且發送器被調用後,就發生進程切換。不過,在非搶佔核心中,當前進程是不可能被替換的,除非它打算切換到使用者態,即從系統調用或中斷中返回。
所以,搶佔核心的主要特點是:一個在核心態啟動並執行進程,若且唯若在執行核心功能期間被另外一個進程取代。
如果還沒看明白,讓我們舉一對執行個體來說明搶佔核心和非搶佔核心的區別:進程A執行例外處理常式時(肯定是在核心態),一個具有較高優先順序的進程變為可執行狀態。這種情況是可能出現的,因為,有可能某個裝置,如鍵盤發生了插斷要求而且相應的處理常式喚醒了進程B。如果核心是搶佔的,就會發生強制性進程切換,讓進程B取代進程A。例外處理常式的執行被暫停,直到發送器再次選擇進程A時才恢複它的執行(B進程執行完畢後不一定馬上恢複到A,要看發送器的選擇,這就是前邊提到的第四種情況我們所產生的疑問的答案)。相反,如果核心是非搶佔的,在進程A完成例外處理常式的執行之前是不會發生進程切換的,除非進程A回到使用者態,或者自動放棄CPU。
再看另外一個例子,我們考慮一個執行例外處理常式的進程已經用完了它的時間配額(參見“scheduler_tick()函數”博文)的情況。如果核心是搶佔的,進程可能會立即被取代,但如果核心是非搶佔的,進程繼續運行直到它執行完例外處理常式或自動放棄CPU。
說了這麼多,那麼Linux 2.6為啥要設定核心搶佔這麼個機制呢?
使核心可搶佔的目的在於減少使用者態進程的指派延遲(dispatch latency),即減少從進程變為可執行狀態到它實際開始運行之間的時間間隔。核心搶佔對執行及時被調度的任務(如:硬體控制器、環境監視器、電影播放器等等)的進程確實是有好處的,因為它降低了這種進程被另一個運行在核心態的進程拖延的風險。
使Linux 2.6核心具有可搶佔的特性並不用對支援非搶佔的舊核心在設計上做太大的改變,當被current_thread_info()宏所引用的thread_info描述符的preempt_count欄位大於0時,就禁止核心搶佔,即對應的進程必須回到使用者態或主動放棄CPU才能被其他核心態進程搶佔。Linux對該欄位的編碼對應三個不同的計數器,對應以下三種情況發生時,取值都大於0:
1. 核心正在執行插斷服務常式。
2. 可延遲函數被禁止(當核心正在執行非強制中斷或tasklet時經常如此)。
3. 通過把搶佔計數器設定為正數而顯式地禁用核心搶佔。
上面的原則告訴我們:只有當核心正在執行例外處理常式(尤其是系統調用),而且核心搶佔沒有被顯式地禁用時,才可能搶佔核心。此外,別忘了本地CPU開啟本地中斷,否則無法完成核心搶佔。
核心提供一些專門的簡單宏,來處理preempt_count欄位的搶佔計數器:
preempt_count —— 在thread_info描述符中選擇preempt_count欄位
preempt_disable —— 使搶佔計數加1
preempt_enable_no_resched —— 使搶佔計數減1
preempt_enable —— 使搶佔計數器的值減1,並在thread _info描述符的TIF_NEED_RESCHED標誌被置為1的情況下,調用preempt_schedule()
get_cpu —— 與preempt_disable相似,但要返回本地CPU的數量
put_cpu —— 與preempt_enable相同
put_cpu_no_resched —— 與preempt_enable_no_resched相同
這裡我們只是重點介紹一下preempt_enable()宏,它遞減搶佔計數器,然後檢查當前thread_info的flags欄位中的TIF_NEED_RESCHED標誌位是否被設定。如果被以前的某個時刻設定過,則說明進程切換請求是掛起的,因此宏調用preempt_schedule()函數,它本質上執行下面的代碼:
if (!current_thread_info->preempt_count && !irqs_disabled()) {
current_thread_info->preempt_count = PREEMPT_ACTIVE;
schedule();
current_thread_info->preempt_count = 0;
}
該函數檢查是否允許本地中斷,以及當前進程的preempt_count欄位是否為0,如果兩個條件都為真,它就調用schedule()選擇另外一個進程來運行。因此,核心搶佔可能在結束核心控制路徑(通常是一個中斷處理常式)時發生,也可能在例外處理常式調用preempt_enable()重新允許核心搶佔時發生。
-
最後,我鄭重提醒大家:核心搶佔會引起不容忽視的開銷。因此,Linux 2.6獨具特色地允許使用者在編譯核心時通過設定選項來禁用或啟用核心搶佔——#define CONFIG_PREEMPT
核心搶佔會發生在:
1.當從中斷處理常式正在執行,且返回核心空間之前(此時可搶佔標誌premptcount須為0,need_resched被置位);
2.當核心代碼再一次具有可搶佔性的時候,如解鎖及使能非強制中斷等;
3.如果核心中的任務顯式地調用schedule();
4.如果內核心中的任務阻塞(這同樣也會導致調用schedule())