進程調度之6:進程的調度與切換

date: 2014-10-31 12:16linux

1 linux的調度機制

在討論進程的調度與切換時,咱們關注以下幾個問題:安全

  1. 切換的時機:在何時進行切換
  2. 調度策略(policy):根據什麼準則挑選下一個進行運行的進程
  3. 調度的方式:是可剝奪(preemptive)仍是不可剝奪(nonpreemptive)。當正在運行的進程沒有覺悟自願放棄對CPU的使用權時,是否能夠強制性的暫時剝奪其使用權,中止其運行而給其餘進程一個機會?若是可剝奪,是否任何條件下均可剝奪,有沒有例外?

那麼linux內核的調度機制是怎樣的呢?先來看看進程狀態轉換關係示意圖:app

進程狀態機

1.1 調度的時機分兩種狀況

首先自願的調度隨時均可以進行。在內核空間中,一個進程能夠隨時經過調用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

1.2 調度方式爲「有條件可剝奪」方式

當進程在用戶空間中運行時,無論自願不自願,一旦有必要(好比運行太長時間),內核就能夠暫時剝奪其運行轉而調度其餘進程運行。但是,一旦進程進入內核空間,就像進入「安全地帶」,這時,儘管內核知道要調度了,也只能乾等着,等待進程離開「安全地帶」返回用戶空間前夕將其剝奪。所以說,linux的調度方式是可剝奪的,但因爲剝奪時機的限制而變成有條件可剝奪的了。 那麼,剝奪式的調用發生在何時呢?一樣是進程從系統空間返回用戶空間的前夕。設計

其實,這裏討論「有條件可剝奪」與前面的調度時機是密切相關的,剝奪式的調度即非自願的強制調度,它剝奪當前進程的運行權利而讓其餘進程運行。3d

1.3 調度政策

調度政策爲以優先級爲基礎的調度。內核爲每一個進程計算出一個反應其運行資格的權值,而後挑選權值最高的進程投入運行。而資格的運算則是以優先級爲基礎。unix

爲了適應不一樣的需求,內核實現了三種不一樣的政策:SCHED_FIFO、SCHED_RR以及SCHED_OTHER。SCHED_FIFO適應於實時性要求比較強、而每次運行的耗時又比較短的進程;SCHED_RR適用於實時性要求較高但每次運行耗時較長的進程,其中的RR表示「Round Robin」即輪流之意,意即當多個進程具備同一優先級時,輪流調度運行;SCHED_OTHER則爲傳統的調度政策,適用與交互式的分時應用。 既然每一個進程都有本身使用的調度政策,那麼在計算運行資格時涉及到「歸一化」的問題,即在計算資格時將政策也考慮進去,就像高考時,給符合某條件的考生加分同樣。計算資格的函數爲goodness,咱們在後面會詳細講到。指針

2 schedule函數流程以及進程切換過程

2.1 主要流程

在exit一節中,一個即將去世的進程在do_exit中的最後一件事就是調用schedule自願讓出CPU,這是自願調度的情形;此外,每當系統調用(或者是中斷)返回到用戶空間的前夕,內核會檢查當前進程的need_resched字段,若是該字段非0,則調用schedule()進行一次強制性調度(這部分代碼在<arch/i386/kernel/entry.s>中),這是強制調度的情形。

本小節咱們來看看schedule的流程,其定義在<kernel/sched.c>文件中,流程圖以下(源代碼裏用了大量的goto 語句,這裏爲了描述方便,在不影響流程的狀況下,省略對這些跳轉描述):

schedule流程

2.2 active_mm

進程的task_struct結構中有兩個mm_struct結構指針:一個是mm,指向進程的用戶空間,另外一個是active_mm。對於具備用戶空間的進程這兩個指針是一致的(好比在execve系統調用中會設置成一致,參考exec_mmap函數的詳細代碼);可是當一個不具有用戶空間的內核進程被調度運行時,要求它必須有一個active_mm,因此只好借用一個。問誰借呢,最簡單就是爲借用當前進程(即將被換出的進程)的active_mm(當前進程也多是是個內核進程,它的active_mm也多是借來的),由於這樣能夠省去用戶空間切換的開銷,而在該進程被換成中止運行時,要記得歸還它借來的active_mm。

爲何必需要有一個active_mm?由於指向頁面映射目錄表的指針pgd就在這個結構中,內核進程不是沒有用戶空間嗎,它要pgd何用?不要忘了,目錄表中除了有用戶空間的虛存頁面映射,還有內核空間的虛存頁面映射,參考第2章第6節。

2.3 中斷上下文

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,則說明停車場內還有車。

2.4 goodness函數以及進程運行資格的計算

/*
     * 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;
    }

這個函數比較簡單,不一樣調度政策運行資格的計算可參考以下表格:

goodness

回到流程圖中,若是遍歷完可運行隊列中全部的進程,那麼候選進程的運行資格c的值有以下幾種可能:

goodness返回值

爲進程從新分配時間配的代碼以下:

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的進程通過長期的「韜光養晦」,其運行資格也沒法超過實時調度政策的進程。

2.5 switch_to

任務切換的核心爲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的操做以下圖所示:

switch_to示意圖

咱們再來逐行說明下:

  • 第16~18行,備份寄存器esi、edi與ebp到進程A的系統空間堆棧中;第19行,將進程A系統空間堆棧的棧頂位置備份到其task_struct結構下屬的thread_struct結構中;
  • 第20行,將進程B備份的系統空間堆棧棧頂位置裝載進esp寄存器,這樣便完成了系統空間堆棧的切換,此時current宏所表明的進程就是進程B了。咱們以動態的眼光來看,進程B以前確定也被換出過,也曾作過「進程A」,意即也曾「趟過」第16~19行代碼,那麼其系統空間堆棧中確定已經備份了寄存器esi、edi與ebp,而其esp指向備份寄存器後的棧頂(這裏有一例外,就是fork產生的子進程初次運行時,見後文分析)。
  • 第21行,設置進程A系統空間的eip(一樣存儲在task_struct結構下屬的thread_struct結構中)爲標號1的地址。進程A被暫停運行,下一次被調度運行,將從標號1處開始執行(參考後續分析)。
  • 第22行,進程B的eip入棧。一樣,用動態的眼光來看,進程B的備份的eip也指向標號1處。注意,這裏有一個例外,那就是fork產生的子進程。還記得fork系統調用中的copy_thread()函數嗎,在該函數中將子進程的eip設置爲ret_from_fork(定義在<arch/i386/kernel/entry.s>中的第179行)。
  • 第23行,經過jmp指令調用__switch_to()函數,因爲不是經過call指令來調用子函數(call指令調用子函數時,會將子函數的返回地址即call指令的下一條指令的地址入棧),那麼第22行入棧的eip即爲__switch_to()函數的返回地址。那麼當從__switch_to()返回後,進程B接着從標號1處開始執行。

咱們來看看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並不買帳。由於這個「大而全」硬件切換一是缺乏靈活性,二是切換速度不必定快(由於有不少多餘的切換動做)。

相關文章
相關標籤/搜索