轉自:http://linuxperf.com/?p=211
進程切換有自願(Voluntary)和強制(Involuntary)之分,在前文中詳細解釋了兩者的不同,簡單來說,自願切換意味着進程需要等待某種資源,強制切換則與搶占(Preemption)有關。
搶占(Preemption)是指內核強行切換正在CPU上運行的進程,在搶占的過程中並不需要得到進程的配合,在隨后的某個時刻被搶占的進程還可以恢復運行。發生搶占的原因主要有:進程的時間片用完了,或者優先級更高的進程來爭奪CPU了。
搶占的過程分兩步,第一步觸發搶占,第二步執行搶占,這兩步中間不一定是連續的,有些特殊情況下甚至會間隔相當長的時間:
- 觸發搶占:給正在CPU上運行的當前進程設置一個請求重新調度的標志(TIF_NEED_RESCHED),僅此而已,此時進程並沒有切換。
- 執行搶占:在隨后的某個時刻,內核會檢查TIF_NEED_RESCHED標志並調用schedule()執行搶占。
搶占只在某些特定的時機發生,這是內核的代碼決定的。
觸發搶占的時機
每個進程都包含一個TIF_NEED_RESCHED標志,內核根據這個標志判斷該進程是否應該被搶占,設置TIF_NEED_RESCHED標志就意味着觸發搶占。
直接設置TIF_NEED_RESCHED標志的函數是 set_tsk_need_resched();
觸發搶占的函數是resched_task()。
TIF_NEED_RESCHED標志什么時候被設置呢?在以下時刻:
- 周期性的時鍾中斷
時鍾中斷處理函數會調用scheduler_tick(),這是調度器核心層(scheduler core)的函數,它通過調度類(scheduling class)的task_tick方法 檢查進程的時間片是否耗盡,如果耗盡則觸發搶占:
Linux的進程調度是模塊化的,不同的調度策略比如CFS、Real-Time被封裝成不同的調度類,每個調度類都可以實現自己的task_tick方法,調度器核心層根據進程所屬的調度類調用對應的方法,比如CFS對應的是task_tick_fair,Real-Time對應的是task_tick_rt,每個調度類對進程的時間片都有不同的定義。
- 喚醒進程的時候
當進程被喚醒的時候,如果優先級高於CPU上的當前進程,就會觸發搶占。相應的內核代碼中,try_to_wake_up()最終通過check_preempt_curr()檢查是否觸發搶占。
- 新進程創建的時候
如果新進程的優先級高於CPU上的當前進程,會觸發搶占。相應的調度器核心層代碼是sched_fork(),它再通過調度類的 task_fork方法觸發搶占:
- 進程修改nice值的時候
如果進程修改nice值導致優先級高於CPU上的當前進程,也會觸發搶占。內核代碼參見 set_user_nice()。
- 進行負載均衡的時候
在多CPU的系統上,進程調度器盡量使各個CPU之間的負載保持均衡,而負載均衡操作可能會需要觸發搶占。
不同的調度類有不同的負載均衡算法,涉及的核心代碼也不一樣,比如CFS類在load_balance()中觸發搶占:
RT類的負載均衡基於overload,如果當前運行隊列中的RT進程超過一個,就調用push_rt_task()把進程推給別的CPU,在這里會觸發搶占。
執行搶占的時機
觸發搶占通過設置進程的TIF_NEED_RESCHED標志告訴調度器需要進行搶占操作了,但是真正執行搶占還要等內核代碼發現這個標志才行,而內核代碼只在設定的幾個點上檢查TIF_NEED_RESCHED標志,這也就是執行搶占的時機。
搶占如果發生在進程處於用戶態的時候,稱為User Preemption(用戶態搶占);如果發生在進程處於內核態的時候,則稱為Kernel Preemption(內核態搶占)。
執行User Preemption(用戶態搶占)的時機
- 從系統調用(syscall)返回用戶態時;
- 從中斷返回用戶態時。
執行Kernel Preemption(內核態搶占)的時機
Linux在2.6版本之后就支持內核搶占了,但是請注意,具體取決於內核編譯時的選項:
- CONFIG_PREEMPT_NONE=y
不允許內核搶占。這是SLES的默認選項。 - CONFIG_PREEMPT_VOLUNTARY=y
在一些耗時較長的內核代碼中主動調用cond_resched()讓出CPU。這是RHEL的默認選項。 - CONFIG_PREEMPT=y
允許完全內核搶占。
在 CONFIG_PREEMPT=y 的前提下,內核態搶占的時機是:
- 中斷處理程序返回內核空間之前會檢查TIF_NEED_RESCHED標志,如果置位則調用preempt_schedule_irq()執行搶占。preempt_schedule_irq()是對schedule()的包裝。
- 當內核從non-preemptible(禁止搶占)狀態變成preemptible(允許搶占)的時候;
在preempt_enable()中,會最終調用 preempt_schedule 來執行搶占。preempt_schedule()是對schedule()的包裝。