date: 2014-10-31 12:16linux
在討論進程的調度與切換時,咱們關注以下幾個問題:安全
那麼linux內核的調度機制是怎樣的呢?先來看看進程狀態轉換關係示意圖:app
首先自願的調度隨時均可以進行。在內核空間中,一個進程能夠隨時經過調用schedule來啓動一次調度。在用戶空間中,進程能夠經過系統調用pause來自願讓出cpu從而啓動一次調度。從應用的角度來看,只有在用戶空間自願放棄(pause系統調用以及nanosleep系統調用,注意sleep不是系統調用而是C庫函數)這一舉動是可見的;而進程陷入內核後的自願放棄行爲是不可見的,它隱藏在其餘可能受阻的系統調用,好比open、read、write等。進程因這些系統調用而陷入內核,若是這些調用被阻塞,總不能讓CPU阻塞在這裏啥都不幹吧,因而內核就替進程作主,自願放棄CPU啓動一次調度。ide
此外,若是一個進程運行太長時間,調度器可能會進行一次強制調度。非自願的被強制的調度(發生在每次從系統調用返回到用戶空間的前夕,以及每次從中斷或異常處理返回到用戶空間的前夕。注意這裏的「返回到用戶空間的前夕」的限定,對系統調用來講,確定是返回到用戶空間了;對中斷或異常來講,它有可能發生在用戶空間(當進程在用戶空間運行時中斷來了),也可能發生在內核空間(即當進程陷入內核後,中斷來了),那麼中斷有可能返回到用戶空間也有可能返回到內核空間。有了這個限定之後,只有當在用戶空間發生的中斷,其返回到用戶空間前夕,纔會進行一次強制調度;而在內核空間發生的中斷,其返回時不會進行強轉調度。這就給內核的設計與實現帶來了便利。想一想看,若是沒有這個限定的話,在內核空間中,當前進程可能由於中斷而被強轉換出,其正在使用的資源可能會被新運行的進程所修改,這樣一來,全部在進程間共享的數據都要經過互斥來保護了,這種多進程共享的數據何其多矣,加不勝加呀。函數
還要指明,強制調度還有一個條件,那就是當前進程task_struct結構的need_resched字段被置1(前面講fork流程時,父進程將本身的need_resched置1,所以,從fork返回時會發生一次強制性調度),那麼誰來設置該字段了,天然只有內核了,用戶空間沒法訪問到task_struct結構的。什麼狀況下設置該字段呢?其一,在某些系統調用的內核實現中設置,好比系統調用pause、fork中,還有其餘調用可能受阻的系統調中;其二在時鐘中斷服務程序中,發現當前進程運行過久時設置;其三,內核中因某種緣由喚醒一個進程時。this
當進程在用戶空間中運行時,無論自願不自願,一旦有必要(好比運行太長時間),內核就能夠暫時剝奪其運行轉而調度其餘進程運行。但是,一旦進程進入內核空間,就像進入「安全地帶」,這時,儘管內核知道要調度了,也只能乾等着,等待進程離開「安全地帶」返回用戶空間前夕將其剝奪。所以說,linux的調度方式是可剝奪的,但因爲剝奪時機的限制而變成有條件可剝奪的了。 那麼,剝奪式的調用發生在何時呢?一樣是進程從系統空間返回用戶空間的前夕。設計
其實,這裏討論「有條件可剝奪」與前面的調度時機是密切相關的,剝奪式的調度即非自願的強制調度,它剝奪當前進程的運行權利而讓其餘進程運行。3d
調度政策爲以優先級爲基礎的調度。內核爲每一個進程計算出一個反應其運行資格的權值,而後挑選權值最高的進程投入運行。而資格的運算則是以優先級爲基礎。unix
爲了適應不一樣的需求,內核實現了三種不一樣的政策:SCHED_FIFO、SCHED_RR以及SCHED_OTHER。SCHED_FIFO適應於實時性要求比較強、而每次運行的耗時又比較短的進程;SCHED_RR適用於實時性要求較高但每次運行耗時較長的進程,其中的RR表示「Round Robin」即輪流之意,意即當多個進程具備同一優先級時,輪流調度運行;SCHED_OTHER則爲傳統的調度政策,適用與交互式的分時應用。 既然每一個進程都有本身使用的調度政策,那麼在計算運行資格時涉及到「歸一化」的問題,即在計算資格時將政策也考慮進去,就像高考時,給符合某條件的考生加分同樣。計算資格的函數爲goodness,咱們在後面會詳細講到。指針
在exit一節中,一個即將去世的進程在do_exit中的最後一件事就是調用schedule自願讓出CPU,這是自願調度的情形;此外,每當系統調用(或者是中斷)返回到用戶空間的前夕,內核會檢查當前進程的need_resched字段,若是該字段非0,則調用schedule()進行一次強制性調度(這部分代碼在<arch/i386/kernel/entry.s>中),這是強制調度的情形。
本小節咱們來看看schedule的流程,其定義在<kernel/sched.c>文件中,流程圖以下(源代碼裏用了大量的goto 語句,這裏爲了描述方便,在不影響流程的狀況下,省略對這些跳轉描述):
進程的task_struct結構中有兩個mm_struct結構指針:一個是mm,指向進程的用戶空間,另外一個是active_mm。對於具備用戶空間的進程這兩個指針是一致的(好比在execve系統調用中會設置成一致,參考exec_mmap函數的詳細代碼);可是當一個不具有用戶空間的內核進程被調度運行時,要求它必須有一個active_mm,因此只好借用一個。問誰借呢,最簡單就是爲借用當前進程(即將被換出的進程)的active_mm(當前進程也多是是個內核進程,它的active_mm也多是借來的),由於這樣能夠省去用戶空間切換的開銷,而在該進程被換成中止運行時,要記得歸還它借來的active_mm。
爲何必需要有一個active_mm?由於指向頁面映射目錄表的指針pgd就在這個結構中,內核進程不是沒有用戶空間嗎,它要pgd何用?不要忘了,目錄表中除了有用戶空間的虛存頁面映射,還有內核空間的虛存頁面映射,參考第2章第6節。
schedule只能由進程在內核空間中主動調用,或者在當進程從系統空間返回到用戶空間前夕被動地調用,而不能在一箇中斷服務程序內部調用。即便一箇中斷服務程序有調度的要求,也只能經過設置當前進程的need_resched字段爲1來表達這種需求,而不能直接調用schedule。那麼怎麼判斷當前處在中斷上下文(即在中斷服務程序裏還沒出來)呢?咱們來看看in_interrupt的定義。
<include/asm/hardirq.h> 20 /* 21 * Are we in an interrupt context? Either doing bottom half 22 * or hardware interrupt processing? 23 */ 24 #define in_interrupt() ({ int __cpu = smp_processor_id(); \ 25 (local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
在單CPU系統中,__cpu爲0。在中斷服務的入口和出口出,分別會調用irq_enter()和irq_exit()來遞增和遞減計數器local_irq_count[__cpu],只要這個計數器非0,就說明CPU在中斷服務程序中還未離開。相似的,只要計數器local_bh_count[__cpu]非0就說明CPU在執行某個bh函數。就像停車場,每開入一輛車計算加1,每開出一輛車計數器減1,若是這個計數器非0,則說明停車場內還有車。
/* * This is the function that decides how desirable a process is.. * You can weigh different processes against each other depending * on what CPU they've run on lately etc to try to handle cache * and TLB miss penalties. * * Return values: * -1000: never select this * 0: out of time, recalculate counters (but it might still be selected) * +ve: "goodness" value (the larger, the better) * +1000: realtime process, select this. */ static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm) { int weight; /* * select the current process after every other * runnable process, but before the idle thread. * Also, dont trigger a counter recalculation. */ weight = -1; if (p->policy & SCHED_YIELD) goto out; /* * Non-RT process - normal case first. */ if (p->policy == SCHED_OTHER) { /* * Give the process a first-approximation goodness value * according to the number of clock-ticks it has left. * * Don't do any other calculations if the time slice is * over.. */ weight = p->counter; if (!weight) goto out; /* .. and a slight advantage to the current MM */ if (p->mm == this_mm || !p->mm) weight += 1; weight += 20 - p->nice; goto out; } /* * Realtime process, select the first one on the * runqueue (taking priorities within processes * into account). */ weight = 1000 + p->rt_priority; out: return weight; }
這個函數比較簡單,不一樣調度政策運行資格的計算可參考以下表格:
回到流程圖中,若是遍歷完可運行隊列中全部的進程,那麼候選進程的運行資格c的值有以下幾種可能:
爲進程從新分配時間配的代碼以下:
for_each_task(p) p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
宏NICE_TO_TICKS的定義以下。參考註釋,做者的意圖是但願NICE_TO_TICKS獲得的時間片在50ms左右,所以須要根據時鐘頻率HZ來定義。好比,若是HZ爲200,表示每秒中斷200次,那麼一個滴答tick爲5ms,20-(nice)的取值爲[1 ,40],平均值爲20,將20右移1位即除以2爲10,10個滴答即50ms。當時鍾頻率HZ越高,每一個滴答所表明的時間越短,NICE_TO_TICKS分配的滴答數越多,但最大隻是20-(nice)的值左移2位即乘以4,極大值爲160,這是沒法與實時調度政策中最低運行資格爲1000相抗衡的。
/* * Scheduling quanta. * * NOTE! The unix "nice" value influences how long a process * gets. The nice value ranges from -20 to +19, where a -20 * is a "high-priority" task, and a "+10" is a low-priority * task. * * We want the time-slice to be around 50ms or so, so this * calculation depends on the value of HZ. */ #if HZ < 200 #define TICK_SCALE(x) ((x) >> 2) #elif HZ < 400 #define TICK_SCALE(x) ((x) >> 1) #elif HZ < 800 #define TICK_SCALE(x) (x) #elif HZ < 1600 #define TICK_SCALE(x) ((x) << 1) #else #define TICK_SCALE(x) ((x) << 2) #endif #define NICE_TO_TICKS(nice) (TICK_SCALE(20-(nice))+1)
另外,須要說明,在從新計算時間配額時,對全部進程都進行了更新。並且更新是將原有配額除以2再加上NICE_TO_TICKS。那麼那些不在可運行隊列中的調度政策爲SCHED_OTHER的進程,會所以得到較高的時間配額,在未來的調度中會佔必定的優點。但這種更新方式也決定了更新後的時間配額不會超過兩倍的NICE_TO_TICKS。所以即便調度政策爲SCHED_OTHER的進程通過長期的「韜光養晦」,其運行資格也沒法超過實時調度政策的進程。
任務切換的核心爲switch_to,這是一段嵌入式彙編代碼,定義在<include/asm/system.h>文件中:
#define switch_to(prev,next,last) do { \ asm volatile("pushl %%esi\n\t" \ "pushl %%edi\n\t" \ "pushl %%ebp\n\t" \ "movl %%esp,%0\n\t" /* save ESP */ \ "movl %3,%%esp\n\t" /* restore ESP */ \ "movl $1f,%1\n\t" /* save EIP */ \ "pushl %4\n\t" /* restore EIP */ \ "jmp __switch_to\n" \ "1:\t" \ "popl %%ebp\n\t" \ "popl %%edi\n\t" \ "popl %%esi\n\t" \ :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ "=b" (last) \ :"m" (next->thread.esp),"m" (next->thread.eip), \ "a" (prev), "d" (next), \ "b" (prev)); \ } while (0)
switch_to()有三個參數,schedule()調用它時,第一個和第三個參數傳入的是當前進程,即要被調度器換出的進程,設爲進程A,第二個參數傳入的是候選進程,及要被調度器調度運行的進程,設爲進程B,咱們用三步法來分析下這段代碼:
;僞寄存器 ; prev->thread.esp --> r0 ; prev->thread.eip --> r1 ; last --> r2 ; next->thread.esp --> r3 ; next->thread.eip --> r4 ;僞寄存器與通用寄存器結合的"建議" ; prev --> eax ; netx --> edx ; prev --> ebx 016 pushl %esi /*在A進程的系統空間堆棧中進行入棧操做*/ 017 pushl %edi 018 pushl %ebp 019 movl %esp, r0 /*保存A進程系統空間堆棧的棧頂指針esp到其task_struct 結構的thread成員中*/ 020 movl r3, %esp /*從B進程task_struct結構的thread成員中恢復B進程的 系統空間堆棧esp,執行該指令以後,系統空間堆棧已經切換 到了B進程的系統空間堆棧 */ 021 move $lf, r1 /*設置A進程下一次調度執行時系統空間eip爲標號1的地址 (保存到A進程task_struct結構的thread成員中)*/ 022 push r4 /*B進程系統空間的eip入棧(此時,固然是B進程的系統空間 堆棧) */ 023 jmp __switch_to /*這裏經過jmp而不是call調用函數__switch_to,因而 __switch_to函數的返回地址就上上條指令壓入的r4,即 進程B系統空間的eip*/ 024 1: 025 popl %ebp /*在B進程的系統空間堆棧中進行出棧操做*/ 026 popl %edi 027 popl %esi
代碼的意圖請參考註釋。這段代碼對進程A與進程B的操做以下圖所示:
咱們再來逐行說明下:
咱們來看看B進程的執行路徑:
25行~27行,寄存器出棧,這與16~18行相對應。因爲switch_to()自己是個宏,編譯後「內嵌」到schedule()函數中。因此接下來進程B繼續在schedule()函數中執行,直到schedule()函數結束後返回。
進程B從schedule()函數返回時,要從B進程的系統空間堆棧彈出返回地址。用動態的眼光來看,B進程當初被暫停運行時,確定也曾調用過schedule(有一個特例,fork系統調用產生的子進程,下文詳述),系統空間堆棧中確定保存着schedule()的返回地址。若是進程B當初在內核代碼中主動調用schecule(),那麼如今將回到schedule()的下一條代碼執行;
若是進程B當初是在從系統空間返回用戶空間前夕被強制調用了schedule(),那麼這時將會回到<arch/i386/kernel/entry.s>中的第289行(見下),調用ret_from_sys_call恢復進程B用戶空間的現場(從進程B系統空間堆棧頂部的pt_regs處恢復),進程B就回到它的用戶空間繼續運行了。
287 reschedule: 288 call SYMBOL_NAME(schedule) # test 289 jmp ret_from_sys_call
咱們再來看看fork產生的子進程首次被調度運行時的運行路線:
第22行,進程B的eip入棧。進程的eip被設置爲ret_from_fork();
第23行,經過jmp指令調用__switch_to()函數。__switch_to()函數的返回地址即爲ret_from_fork()函數的入口,那麼當從__switch_to()函數返回時,直接跳轉到ret_from_fork()處執行,繼而跳轉到ret_from_sys_call處,到達ret_with_reschedule時,因爲子進程的need_resched字段爲0,那麼就直接返回到用戶空間了。這部分代碼在<arch/i386/kernel/entry.s>中第205行到223行。
fork產生的子進程初次被調度時,沒有執行switch_to()的第25行到27行,也不涉及從schedule()函數中返回,它「抄了一段近路」直接到跳轉至ret_from_fork(),而後返回到用戶空間。
至於函數__switch_to(),那只是內核「應付」intel的「硬」任務切換。intel支持由CPU硬件來實施任務切換(核心是TSS),但linux並不買帳。由於這個「大而全」硬件切換一是缺乏靈活性,二是切換速度不必定快(由於有不少多餘的切換動做)。