天天看點

Linux核心态搶占機制分析

本文首先介紹非搶占式核心(Non-Preemptive Kernel)和可搶占式核心(Preemptive Kernel)的差別。接着分析Linux下有兩種搶占:使用者态搶占(User Preemption)、核心态搶占(Kernel Preemption)。然後分析了在核心态下:如何判斷能否搶占核心(什麼是可搶占的條件);何時觸發重新排程(何時設定可搶占條件);搶占發生的時機(何時檢查可搶占的條件);什麼時候不能搶占核心。最後分析了2.6kernel中如何支援搶占核心。

1. 非搶占式和可搶占式核心的差別

為了簡化問題,我使用嵌入式實時系統uC/OS作為例子。首先要指出的是,uC/OS隻有核心态,沒有使用者态,這和Linux不一樣。

多任務系統中,核心負責管理各個任務,或者說為每個任務配置設定CPU時間,并且負責任務之間的通訊。核心提供的基本服務是任務切換。排程(Scheduler),英文還有一詞叫dispatcher,也是排程的意思。這是核心的主要職責之一,就是要決定該輪到哪個任務運作了。多數實時核心是基于優先級排程法的。每個任務根據其重要程度的不同被賦予一定的優先級。基于優先級的排程法指,CPU總是讓處在就緒态的優先級最高的任務先運作。然而,究竟何時讓高優先級任務掌握CPU的使用權,有兩種不同的情況,這要看用的是什麼類型的核心,是不可剝奪型的還是可剝奪型核心。

非搶占式核心

非搶占式核心是由任務主動放棄CPU的使用權。非搶占式排程法也稱作合作型多任務,各個任務彼此合作共享一個CPU。異步事件還是由中斷服務來處理。中斷服務可以使一個高優先級的任務由挂起狀态變為就緒狀态。但中斷服務以後控制權還是回到原來被中斷了的那個任務,直到該任務主動放棄CPU的使用權時,那個高優先級的任務才能獲得CPU的使用權。非搶占式核心如下圖所示。

非搶占式核心的優點有:

中斷響應快(與搶占式核心比較); 允許使用不可重入函數; 幾乎不需要使用信号量保護共享資料。運作的任務占有CPU,不必擔心被别的任務搶占。這不是絕對的,在列印機的使用上,仍需要滿足互斥條件。 

非搶占式核心的缺點有:

任務響應時間慢。高優先級的任務已經進入就緒态,但還不能運作,要等到目前運作着的任務釋放CPU。 非搶占式核心的任務級響應時間是不确定的,不知道什麼時候最高優先級的任務才能拿到CPU的控制權,完全取決于應用程式什麼時候釋放CPU。 

搶占式核心

使用搶占式核心可以保證系統響應時間。最高優先級的任務一旦就緒,總能得到CPU的使用權。當一個運作着的任務使一個比它優先級高的任務進入了就緒态,目前任務的CPU使用權就會被剝奪,或者說被挂起了,那個高優先級的任務立刻得到了CPU的控制權。如果是中斷服務子程式使一個高優先級的任務進入就緒态,中斷完成時,中斷了的任務被挂起,優先級高的那個任務開始運作。搶占式核心如下圖所示。

搶占式核心的優點有:

使用搶占式核心,最高優先級的任務什麼時候可以執行,可以得到CPU的使用權是可知的。使用搶占式核心使得任務級響應時間得以最優化。 

搶占式核心的缺點有:

不能直接使用不可重入型函數。調用不可重入函數時,要滿足互斥條件,這點可以使用互斥型信号量來實作。如果調用不可重入型函數時,低優先級的任務CPU的使用權被高優先級任務剝奪,不可重入型函數中的資料有可能被破壞。 

2. Linux下的使用者态搶占和核心态搶占

Linux除了核心态外還有使用者态。使用者程式的上下文屬于使用者态,系統調用和中斷處理例程上下文屬于核心态。在2.6 kernel以前,Linux kernel隻支援使用者态搶占。

2.1 使用者态搶占(User Preemption)

在kernel傳回使用者态(user-space)時,并且need_resched标志為1時,scheduler被調用,這就是使用者态搶占。當kernel傳回使用者态時,系統可以安全的執行目前的任務,或者切換到另外一個任務。當中斷處理例程或者系統調用完成後,kernel傳回使用者态時,need_resched标志的值會被檢查,假如它為1,排程器會選擇一個新的任務并執行。

中斷和系統調用的傳回路徑(return path)的實作在entry.S中(entry.S不僅包括kernel entry code,也包括kernel exit code)。

2.2 核心态搶占(Kernel Preemption)

在2.6 kernel以前,kernel code(中斷和系統調用屬于kernel code)會一直運作,直到code被完成或者被阻塞(系統調用可以被阻塞)。在 2.6 kernel裡,Linux kernel變成可搶占式。當從中斷處理例程傳回到核心态(kernel-space)時,kernel會檢查是否可以搶占和是否需要重新排程。kernel可以在任何時間點上搶占一個任務(因為中斷可以發生在任何時間點上),隻要在這個時間點上kernel的狀态是安全的、可重新排程的。

3. 核心态搶占的設計

3.1 可搶占的條件

要滿足什麼條件,kernel才可以搶占一個任務的核心态呢?

沒持有鎖。鎖是用于保護臨界區的,不能被搶占。 Kernel code可重入(reentrant)。因為kernel是SMP-safe的,是以滿足可重入性。 

如何判斷目前上下文(中斷處理例程、系統調用、核心線程等)是沒持有鎖的?Linux在每個每個任務的thread_info結構中增加了preempt_count變量作為preemption的計數器。這個變量初始為0,當加鎖時計數器增一,當解鎖時計數器減一。

3.2 核心态需要搶占的觸發條件

核心提供了一個need_resched标志(這個标志在任務結構thread_info中)來表明是否需要重新執行排程。

3.3 何時觸發重新排程

set_tsk_need_resched():設定指定程序中的need_resched标志

clear_tsk need_resched():清除指定程序中的need_resched标志

need_resched():檢查need_ resched标志的值;如果被設定就傳回真,否則傳回假

什麼時候需要重新排程:

時鐘中斷處理例程檢查目前任務的時間片,當任務的時間片消耗完時,scheduler_tick()函數就會設定need_resched标志; 信号量、等到隊列、completion等機制喚醒時都是基于waitqueue的,而waitqueue的喚醒函數為default_wake_function,其調用try_to_wake_up将被喚醒的任務更改為就緒狀态并設定need_resched标志。 設定使用者程序的nice值時,可能會使高優先級的任務進入就緒狀态; 改變任務的優先級時,可能會使高優先級的任務進入就緒狀态; 建立一個任務時,可能會使高優先級的任務進入就緒狀态; 對CPU(SMP)進行負載均衡時,目前任務可能需要放到另外一個CPU上運作; 

3.4 搶占發生的時機(何時檢查可搶占條件)

當一個中斷處理例程退出,在傳回到核心态時(kernel-space)。這是隐式的調用schedule()函數,目前任務沒有主動放棄CPU使用權,而是被剝奪了CPU使用權。 當kernel code從不可搶占狀态變為可搶占狀态時(preemptible again)。也就是preempt_count從正整數變為0時。這也是隐式的調用schedule()函數。 一個任務在核心态中顯式的調用schedule()函數。任務主動放棄CPU使用權。 一個任務在核心态中被阻塞,導緻需要調用schedule()函數。任務主動放棄CPU使用權。 

3.5 禁用/使能可搶占條件的操作

對preempt_count操作的函數有add_preempt_count()、sub_preempt_count()、inc_preempt_count()、dec_preempt_count()。

使能可搶占條件的操作是preempt_enable(),它調用dec_preempt_count()函數,然後再調用preempt_check_resched()函數去檢查是否需要重新排程。

禁用可搶占條件的操作是preempt_disable(),它調用inc_preempt_count()函數。

在核心中有很多函數調用了preempt_enable()和preempt_disable()。比如spin_lock()函數調用了preempt_disable()函數,spin_unlock()函數調用了preempt_enable()函數。

3.6 什麼時候不允許搶占

preempt_count()函數用于擷取preempt_count的值,preemptible()用于判斷核心是否可搶占。

有幾種情況Linux核心不應該被搶占,除此之外,Linux核心在任意一點都可被搶占。這幾種情況是:

核心正進行中斷處理。在Linux核心中程序不能搶占中斷(中斷隻能被其他中斷中止、搶占,程序不能中止、搶占中斷),在中斷例程中不允許進行程序排程。程序排程函數schedule()會對此作出判斷,如果是在中斷中調用,會列印出錯資訊。 核心正在進行中斷上下文的Bottom Half(中斷的下半部)處理。硬體中斷傳回前會執行軟中斷,此時仍然處于中斷上下文中。 核心的代碼段正持有spinlock自旋鎖、writelock/readlock讀寫鎖等鎖,處幹這些鎖的保護狀态中。核心中的這些鎖是為了在SMP系統中短時間内保證不同CPU上運作的程序并發執行的正确性。當持有這些鎖時,核心不應該被搶占,否則由于搶占将導緻其他CPU長期不能獲得鎖而死等。 核心正在執行排程程式Scheduler。搶占的原因就是為了進行新的排程,沒有理由将排程程式搶占掉再運作排程程式。 核心正在對每個CPU“私有”的資料結構操作(Per-CPU date structures)。在SMP中,對于per-CPU資料結構未用spinlocks保護,因為這些資料結構隐含地被保護了(不同的CPU有不一樣的per-CPU資料,其他CPU上運作的程序不會用到另一個CPU的per-CPU資料)。但是如果允許搶占,但一個程序被搶占後重新排程,有可能排程到其他的CPU上去,這時定義的Per-CPU變量就會有問題,這時應禁搶占。 

4. Linux核心态搶占的實作

4.1 資料結構

[cpp] view plain copy

struct thread_info {        struct task_struct  *task;      /* main task structure */        struct exec_domain  *exec_domain;   /* execution domain */        /**        * 如果有TIF_NEED_RESCHED标志,則必須調用排程程式。        */        unsigned long       flags;      /* low level flags */        /**        * 線程标志:        *     TS_USEDFPU:表示程序在目前執行過程中,是否使用過FPU、MMX和XMM寄存器。        */        unsigned long       status;     /* thread-synchronous flags */        /**        * 可運作程序所在運作隊列的CPU邏輯号。        */        __u32           cpu;        /* current CPU */        __s32           preempt_count; /* 0 => preemptable, <0 => BUG */           mm_segment_t        addr_limit; /* thread address space:                              0-0xBFFFFFFF for user-thead                              0-0xFFFFFFFF for kernel-thread                           */        struct restart_block    restart_block;            unsigned long           previous_esp;   /* ESP of the previous stack in case                              of nested (IRQ) stacks                           */        __u8            supervisor_stack[0];    };    

4.2 代碼流程

禁用/使能可搶占條件的函數

#ifdef CONFIG_DEBUG_PREEMPT      extern void fastcall add_preempt_count(int val);      extern void fastcall sub_preempt_count(int val);    #else    # define add_preempt_count(val) do { preempt_count() += (val); } while (0)    # define sub_preempt_count(val) do { preempt_count() -= (val); } while (0)    #endif        #define inc_preempt_count() add_preempt_count(1)    #define dec_preempt_count() sub_preempt_count(1)       /**    * 在thread_info描述符中選擇preempt_count字段    */    #define preempt_count() (current_thread_info()->preempt_count)       #ifdef CONFIG_PREEMPT       asmlinkage void preempt_schedule(void);        /**    * 使搶占計數加1    */    #define preempt_disable()     do {         inc_preempt_count();         barrier();     } while (0)       /**    * 使搶占計數減1    */    #define preempt_enable_no_resched()     do {         barrier();         dec_preempt_count();     } while (0)        #define preempt_check_resched()     do {         if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))             preempt_schedule();     } while (0)        /**    * 使搶占計數減1,并在thread_info描述符的TIF_NEED_RESCHED标志被置為1的情況下,調用preempt_schedule()    */    #define preempt_enable()     do {         preempt_enable_no_resched();         preempt_check_resched();     } while (0)        #else        #define preempt_disable()       do { } while (0)    #define preempt_enable_no_resched() do { } while (0)    #define preempt_enable()        do { } while (0)    #define preempt_check_resched()     do { } while (0)        #endif    

設定need_resched标志的函數

static inline void set_tsk_need_resched(struct task_struct *tsk)    {        set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);    }        static inline void clear_tsk_need_resched(struct task_struct *tsk)    {        clear_tsk_thread_flag(tsk,TIF_NEED_RESCHED);    }   

點贊 0

繼續閱讀