韓巖___第8課___《linux內核分析》MOOC課

 

Linux進程切換與進程調度時機

1 概述

在多進程的操作系統中,進程調度是一個全局性的、關鍵性的問題,它對系統的總體設計、系統的實現、功能的設置以及各方面的性能都有着決定性的影響。那麼,需要考慮的具體問題主要有:

(1)調度的時機:在什麼情況下、什麼時候進程調度。

(2)如何實現進程間的切換

下面將具體說明。

2 調度時機

進程調度分爲自願和非自願兩種。

2.1 自願調度

自願的調度隨時都可以進行。一個進程可以通過schedule( )啓動一次調度,也可以在調用schedule( )之前,將本進程的狀態置爲TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE,暫時放棄運行而進入睡眠。

從應用的角度看,只有用戶空間自願放棄運行時可見的;而在內核中自願放棄運行則是不可見的,它隱藏在其他可能受阻的系統調用中。幾乎所有涉及到外設的系統調用,如open( )read( )write( )select( )等,都是可能受阻的。

2.2 非自願調度

非自願調度,即強制地發生在每次從系統調用返回的前夕,以及每次從中斷或異常處理返回到用戶空間的前夕。需要注意的是,只有在用戶空間(當CPU在用戶空間運行時)發生的中斷或異常纔會引起調度。

從系統空間返回到用戶空間只是發生調度的必要條件,而不是充分條件。具體是否發生調度還要看有無此種要求。只有在當進程的task_struct結構中的need_resched字段爲非0時纔會轉到reschedule處調用schedule( )need_resched這個字段由內核設置。

下面,將選擇一個主動調度的例子來分析進程的調度和切換過程。

3 進程切換

主動調度,也就是由當前進程自願調用schedule( )暫時放棄運行。我們選擇的例子是進程調用exit( )時的情況。一個正在結束的進程在do_exit( )中的最後一件事情就是調用schedule( ),見do_exit( )代碼。

3.1 schedule( )

schedule( )代碼在/kernel/sched.c中,如下圖所示。

 

schedule( )只能由進程在內核中主動調用,或者是當前進程從系統空間返回用戶空間的前夕被動地發生,而不能再一箇中斷服務程序內部發生。即使一箇中斷服務程序有調度的要求,也只能通過吧當前進程的need_resched字段設爲1來表達這種要求,而不能直接調用schedule( )

schedule( )裏主要調用的宏或函數依次爲:

schedule() –>context_switch() –> switch_to –> __switch_to()

schedule是主調度函數,涉及到具體的調度算法。當schedule()需要暫停A進程的執行而繼續B進程的執行時,就發生了進程之間的切換。

進程切換主要有兩部分:

(1)切換全局頁表項,這個切換工作由context_switch()完成。

(2)切換內核堆棧和硬件上下文,其中switch_to__switch_to()主要完成第二部分。更詳細的,__switch_to()主要完成硬件上下文切換,switch_to主要完成內核堆棧切換。

3.2 switch_to

所謂進程的切換主要是堆棧的切換,這是由宏操作switch_to( )完成的,定義於include/asm_i386/system.h

switch_to的參數prevnextlast不是值拷貝,而是它的調用者context_switch()的局部變量。局部變量是通過%ebp寄存器來索引的,也就是通過n(%ebp)n是編譯時決定的,在不同的進程的同一段代碼中,同一局部變量的n是相同的。有關局部變量如何索引的問題,可以參考這裏和這裏。switch_to中,發生了堆棧的切換,即ebp發生了改變,所以要格外留意在任一時刻的局部變量屬於哪一個進程。關於__switch_to()個函數的調用,並不是通過普通的call來實現,而是直接jmp,函數參數也並不是通過堆棧來傳遞,而是通過寄存器來傳遞。

進程切換髮生在內核態模式,由於進程從用戶態陷入內核態時已經將用戶進程用到通用寄存器的值,用戶態的一些特殊寄存器cseipssesp等保存在進程的內核堆棧之中,因此在內核態進行進程切換的主要工作是完成內核堆棧的切換和相關硬件上下文的切換。

prenext分別是切換前後的兩個進程,那麼這個切換主要需要完成的工作有:

(1)首先需要將CPUesp寄存器的值保存到pre進程中,然後再將CPUesp設置爲 next進程的esp(因爲內核通過esp識別當前運行的進程,內核堆棧棧頂指針的切換通常意味着進程的切換)。

(2)要更新Task State Segment (TSS),更新的變量是esp0(內核堆棧指針)和IO許可權限位圖。對於Linux系統來說同一個CPU上所有的進程共用一個TSS,進程切換了,因此TSS需要隨之改變。Linux系統中主要從兩個方面用到了TSS一是任何進程從用戶態陷入內核態都必須從TSS獲得內核堆棧指針,二是用戶態讀寫IO需要訪TSS的權限位圖。

(3)對於pre進程要將 ebpeflags壓入內核堆棧,對於next進程要將ebpeflags彈出內核堆棧。

下面結合代碼說明切換的過程,一共分四步,圖下圖所示:

第一步:即movl %%esp,%0也就是將寄存器esp中的值保存在進程Athread.esp中。

第二步:即movl %3,%%esp也就是將進程Bthread.esp的值賦給寄存器esp。(實際上這個值就是上一次從B中切換走的時候執行的第一步的結果。爲了要返回,必須爲以後考慮周全。)

第三步:即movl $1f,%1其中1f就是說程序後面標號爲1的地方,將標號爲1的地方的代碼的地址保存到Athread.eip中。

第四步:即pushl %4,將進程Bthread.eip的值壓棧,此時的esp指向已是進程B堆棧。(實際上此時的thread.eip就是上一次從B中切換走的時候第三步執行的結果,即標號一得位置。所以任何進程恢復運行,首先肯定是執行的標號1代碼。)

這裏要說明的是,pushl %4後面的一句代碼是調轉jmp __switch_to__switch_to是個函數,他執行完成以後會有一個ret的操作,即將棧中的第一個地址作爲函數返回的地址,所以就會跳到標號1的地方去執行代碼了。

由於__switch_to的代碼在schedule()中,而shedule()函數又在其他系統調用函數中,比如sys_exit()中,所以先返回到調用B進程上次切換走時的schedule()中,然後返回到調用schedule()的系統調用函數中,最後系統調用又是在用戶空間調用的,所有返回到系統調用的那個地方,接着執行用戶空間的代碼。這樣就徹底的回到了B進程。注意由於此時的返回路徑是根據B堆棧中保存的返回地址來返回的,所以肯定會返回到B進程中。

4 參考資料

[1]毛德操等著.LINUX內核源代碼情景分析(上冊).杭州:浙江大學出版社, 2001.7

[2] http://www.longene.org/forum/viewtopic.php?f=21&t=4646

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章