wangyunqian6的專欄
早期的Linux核心是不可搶佔的。它的排程方法是:一個程序可以通過schedule()函式自願地啟動一次排程。非自願的強制性排程只能發生在每次從系統呼叫返回的前夕以及每次從中斷或異常處理返回到使用者空間的前夕。但是,如果在系統空間發生中斷或異常是不會引起排程的。這種方式使核心實現得以簡化。但常存在下面兩個問題:
- 如果這樣的中斷髮生在核心中,本次中斷返回是不會引起排程的,而要到最初使CPU從使用者空間進入核心空間的那次系統呼叫或中斷(異常)返回時才會發生排程。
- 另外一個問題是優先順序反轉。在Linux中,在核心態執行的任何操作都要優先於使用者態程序,這就有可能導致優先順序反轉問題的出現。例如,一個低優先順序的使用者程序由於執行軟/硬中斷等原因而導致一個高優先順序的任務得不到及時響應。
當前的Linux核心加入了核心搶佔(preempt)機制。核心搶佔指使用者程式在執行系統呼叫期間可以被搶佔,該程序暫時掛起,使新喚醒的高優先順序程序能夠執行。這種搶佔並非可以在核心中任意位置都能安全進行,比如在臨界區中的程式碼就不能發生搶佔。臨界區是指同一時間內不可以有超過一個程序在其中執行的指令序列。在Linux核心中這些部分需要用自旋鎖保護。
核心搶佔要求核心中所有可能為一個以上程序共享的變數和資料結構就都要通過互斥機制加以保護,或者說都要放在臨界區中。在搶佔式核心中,認為如果核心不是在一箇中斷處理程式中,並且不在被 spinlock等互斥機制保護的臨界程式碼中,就認為可以"安全"地進行程序切換。
Linux核心將臨界程式碼都加了互斥機制進行保護,同時,還在執行時間過長的程式碼路徑上插入排程檢查點,打斷過長的執行路徑,這樣,任務可快速切換程序狀態,也為核心搶佔做好了準備。
Linux核心搶佔只有在核心正在執行例外處理程式(通常指系統呼叫)並且允許核心搶佔時,才能進行搶佔核心。禁止核心搶佔的情況列出如下:
(1)核心執行中斷處理例程時不允許核心搶佔,中斷返回時再執行核心搶佔。
(2)當核心執行軟中斷或tasklet時,禁止核心搶佔,軟中斷返回時再執行核心搶佔。
//bh某種意義上也是中斷的一部分,所以必須禁止搶佔。 by melody
(3)在臨界區禁止核心搶佔,臨界區保護函式通過搶佔計數巨集控制搶佔,計數大於0,表示禁止核心搶佔。
//add_preempt_count () ? 可以選擇add 1 << IRQ type ,實際上就算加1之後都不能再搶佔,因為以為著preempt_disable, 表示正在處理搶佔,由於計數值較大,基本上不會再進行搶佔排程. // by melody
搶佔式核心實現的原理是在釋放自旋鎖時或從中斷返回時,如果當前執行程序的 need_resched 被標記,則進行搶佔式排程。
Linux核心線上程資訊結構上增加了成員preempt_count作為核心搶佔鎖,為0表示可以進行核心高度,它隨spinlock和rwlock等一起加鎖和解鎖。執行緒資訊結構thread_info列出如下(在include/asm-x86/thread_info.h中):
struct thread_info { struct task_struct *task; /*主任務結構 */ struct exec_domain *exec_domain; /* 執行的域*/ __u32 flags; /* low level flags */ __u32 status; /* 執行緒同步標識*/ __u32 cpu; /* 當前的CPU */ int preempt_count; /* 0 => 可以搶佔(preemptable), <0 => BUG */ mm_segment_t addr_limit; struct restart_block restart_block; #ifdef CONFIG_IA32_EMULATION void __user *sysenter_return; #endif }; #endif
核心排程器的入口為preempt_schedule(),他將當前程序標記為TASK_PREEMPTED狀態再呼叫schedule(),在TASK_PREEMPTED狀態,schedule()不會將程序從執行佇列中刪除。
核心搶佔API函式
在中斷或臨界區程式碼中,執行緒需要關閉核心搶佔,因此,互斥機制(如:自旋鎖(spinlock)、RCU等)、中斷程式碼、連結串列資料遍歷等需要關閉核心搶佔,臨界程式碼執行完時,需要開啟核心搶佔。關閉/開啟核心搶佔需要使用核心搶佔API函式preempt_disable和preempt_enable。
核心搶佔API函式說明如下(在include/linux/preempt.h中):
preempt_enable() //核心搶佔計數preempt_count減1
preempt_disable() //核心搶佔計數preempt_count加1
preempt_enable_no_resched() //核心搶佔計數preempt_count減1,但不立即搶佔式排程
preempt_check_resched () //如果必要進行排程
preempt_count() //返回搶佔計數
preempt_schedule() //核搶佔時的排程程式的入口點
核心搶佔API函式的實現巨集定義列出如下(在include/linux/preempt.h中):
#define preempt_disable() \ do { \ inc_preempt_count(); \ barrier(); \ //加記憶體屏障,阻止gcc編譯器對記憶體進行優化 } while (0) #define inc_preempt_count() \ do { \ preempt_count()++; \ } while (0) #define preempt_count() (current_thread_info()->preempt_count)
核心搶佔排程
Linux核心在硬中斷或軟中斷返回時會檢查執行搶佔排程。分別說明如下:
(1)硬中斷返回執行搶佔排程
Linux核心在硬中斷或出錯退出時執行函式retint_kernel,執行搶佔函式,函式retint_kernel列出如下(在arch/x86/entry_64.S中):
#ifdef CONFIG_PREEMPT /* 返回到核心空間,檢查是否需要執行搶佔*/ /* 暫存器rcx存放threadinfo地址,此時,中斷關閉*/ ENTRY(retint_kernel) cmpl $0,threadinfo_preempt_count(%rcx) jnz retint_restore_args bt $TIF_NEED_RESCHED,threadinfo_flags(%rcx) jnc retint_restore_args bt $9,EFLAGS-ARGOFFSET(%rsp) /* 中斷是否關閉? */ jnc retint_restore_args call preempt_schedule_irq jmp exit_intr #endif
函式preempt_schedule_irq是出中斷上下文時核心搶佔排程的入口點,該函式被呼叫和返回時中斷應關閉,保護此函式從中斷遞迴呼叫。該函式列出如下(在kernel/sched.c中):
asmlinkage void __sched preempt_schedule_irq(void) { struct thread_info *ti = current_thread_info(); /* 用於捕捉需要修補的呼叫者 */ BUG_ON(ti->preempt_count || !irqs_disabled()); do { /*核心搶佔計數加一個較大的值PREEMPT_ACTIVE,表示正在處理搶佔,由於計數值較大,基本上不會再進行搶佔排程*/ add_preempt_count(PREEMPT_ACTIVE); local_irq_enable(); /*開啟中斷*/ schedule(); /*核心排程,用於核心搶佔,即執行優先順序較高的任務*/ local_irq_disable(); /*關閉中斷*/ sub_preempt_count(PREEMPT_ACTIVE); /*再次檢查,避免在排程與現在時刻之間失去搶佔機會*/ barrier(); /*加記憶體屏障*/ } while (unlikely(test_thread_flag(TIF_NEED_RESCHED))); }
排程函式schedule會檢測程序的 preempt_counter 是否很大,避免普通排程時又執行核心搶佔排程。
(2)軟中斷返回執行搶佔排程
在開啟頁出錯函式pagefault_enable和軟中斷底半部開啟函式local_bh_enable中,會呼叫函式preempt_check_resched檢查是否需要執行核心搶佔。如果不是並能排程,程序才可執行核心搶佔排程。函式preempt_check_resched列出如下:
#define preempt_check_resched() \ do { \ /*如果不是普通排程,才可執行搶佔排程*/ if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \ preempt_schedule(); \ } while (0)