1. 程式人生 > >linux內核:進程切換

linux內核:進程切換

疑問 快速訪問 操作系統 指令 函數 sched sp1 用戶 序列

進程是現代操作系統的核心概念之一,用於分配系統(CPU,內存)資源的使用。 了解linux進程及進程切換的知識,首先要理解進程與程序的區別,進程是執行流,是動態概念;程序是數據與指令序列的集合,是靜態概念。進程作為動態的 執行流,可以用execv系統調用自由選擇一個程序(只要有權限)來執行的,理解這一點很重要。在閱讀本書的第三章《進程》中,有兩個地方比較難於理解 的。

1 switch_to宏的last參數

書中討論switch_to宏(第110頁)時,提到,該宏有3個參數:prev,next和last。前兩個分別是當前進程描述符地址和待切換的進程 描述符的地址,相信大家對這兩個參數都不會有疑問,prev就是從current得到的,而next則是schedule()函數在根據調度算法從進程等 待隊列中挑選的。關鍵是第三個last參數,為什麽需要這麽一個參數呢?書中的描述比較難理解。它的意思是說,A切換到B 時,prev=A,next=B, 經過一定時間後,A被重新調度到CPU上執行時,A需要知道從哪個進程切換過來的,需要從last參數得到。實際上我們只需要關註A->B這一個過 程就可以理解last參數的使用了。下面我們用圖片記錄每個步驟:

技術分享圖片

(1) 在進程切換之前,A是當前進程,esp寄存器指向A的內核棧,prev,next這兩個局部變量保存在棧中,也就是在A的內核棧中。那麽B的內核棧有沒有 這兩個參數呢?當然也有,因為B既然是在等待隊列中,很可能B也經歷過被其他進程切換出去這一個過程,在那個過程中,B的內核棧同樣保存了這兩個變量(如 果B是新創建的進程,可以在創建時,或在schedule函數中將兩個值壓入內核棧),但是這兩個值肯定跟A中的prev=A,next=B不同,因為那 個過程中,B是被切換的,因此,這時,B的內核棧中應該是prev=B.

為了在切換到B進程執行時,prev參數是正確的,就需要借助於第三個參數last 。在schedule函數中(它挑選的B,當然也知道B進程描述符的地址),它從B的進程描述符中得到B的內核棧的地址(書中是thread_info參 數,3.2.54版本代碼中改成了stack參數,原理是一樣的),從而得到B的prev參數的地址,作為第三個參數傳給switch_to宏。 switch_to宏還將A的進程描述符地址加載到EAX寄存器中,而在進程切換過程中,EAX寄存器內容是不會改變的。

(2) 執行進程切換,主要是內核棧的切換,因為內核實現中,將thread_info結構與內核棧放在一起,esp改變了,current參數得到的當前進程描 述符地址也跟著改變。這時,當前進程變成了B進程,並在B的內核棧上工作。註意,這時B內核棧的prev參數還是不正確的,它指向的依然是B。

(3) 將EAX寄存器內容復制到last指向的內存,即B內核棧的prev參數所在的地址。這樣,B內核棧上的prev參數就指向了正確的A進程描述符的地址。

2 進程切換過程中進程棧

書中對進程切換的描述中,對進程的棧的描述是零散的,很容易讓人犯糊途。棧是進程中的重要數據結構,在函數調用中起到核心作用,關於棧的詳細描述可以參閱《深入理解計算機系統》。下面描述進程切換過程中,進程的棧的變遷。

linux的進程有兩種棧,用戶棧和內核棧,它們在不同的內存區域,用戶棧在用戶態中使用,在用戶地址空間分配(0~3G),內核棧在內核態中使用,在 內核地址空間分配(3G~4G)。用戶棧主要用於函數調用和存儲局部變量,內核棧除此之外還要保存進程切換額外的信息,如通用寄存器等。不管是用戶棧還是 內核棧,CPU都是用ESP寄存器保存棧頂地址,因此早在進程切換前,進程進入內核態後,用戶棧就需要被切換出去,整個切換過程,都是在內核棧上工作,因 而用戶棧與進程切換無關。另一方面,內核的實現中,將thread_info結構與內核棧放在一起,內核棧改變了,current參數得到的當前進程描述 符地址也跟著改變,因此進程切換,就是由內核棧切換來完成的。整個完整的進程切換可以分為三個部分,以下假設從進程A切換到進程B:

(1) A的用戶態-->A的內核態

這一過程是由中斷,異常或系統調用實現的,書中的後面章節會有介紹,以後再詳談。這裏只討論幾個要點,每次從用戶態切換到內核態,內核棧都會被清 空,ESP直接指向內核棧的棧底,而用戶棧的信息則會保存到內核棧中。清空內核棧的設計估計是考慮到經過了用戶態的操作後,以前內核棧的調用信息沒有用處 了,沒有必要再保存,畢竟內核棧只分配了8K或4K的空間。那麽,切換到內核態之前,內核怎麽知道進程的內核棧地址呢,進程描述符雖然保存有內核棧的地址 (stack變量),但是進程描述符位於動態內態中,從內存讀取的效率太低了。實現上,它是從TSS中獲取的。

書中“任務狀態段”一節(第108頁)對TSS進行比較詳細的描述,每個CPU都有一個TSS,CPU可以快速訪問它。TSS的一個最重要的功能就是在 用戶態轉為內核態時供CPU讀取內核棧地址,即是init_tss[cpu]->sp0字段(3.2.54版本的代碼),實際上,它存儲的是棧底地 址,因此一加載到ESP中,就同時清空了內核棧。

(2) A的內核態->B的內核態

這一階段實現的是進程間的內核棧切換,同時也實現進程切換。與此過程關系最密切的 是task_struct的thread變量,thread變量的類型是thread_struct,可稱為線程描述符,用於保存進程切換的硬件上下文 (書中第109頁)。書中的switch_to和__switch_to函數詳細描述了進程切換過程中的每一個步驟,與內核棧相關的有:

  • 保存A的內核棧棧頂地址,即ESP寄存器的內容到A_task->thread->sp。(switch_to的第3步,變量名根據3.2.54版本中的代碼)
  • 將B_task->thread->sp內容加載到ESP。(switch_to的第4步,這步完成了內核棧的切換)
  • 將B_task->thread->sp0加載到 init_tss[cpu]->sp0字段(__switch_to的第3步),這一步與(1)的描述對應,以後B在運行期間,用戶態切換到內核態 時,ESP寄存器總是從init_tss[cpu]->sp0字段獲取內核棧的地址,這一操作同時清空了內核棧內容。(thread_struct 結構有sp0,sp1變量,sp0保存內核棧棧底地址,sp保存棧頂地址)。

(3)B的內核態->B用戶態

執行與(1)相反的過程,從內核棧中取出(1)中保存的用戶棧信息,裝載相應寄存器,切換到用戶棧,內核棧信息不必保存,因為(2)中已保存了棧底地址, 下次進入內核棧時直接將其加載到ESP寄存器中即可(將棧底地址作為棧頂使用)。這一過程書中後面的章節同樣會有詳細描述。

linux內核:進程切換