/*
注:在學習內核的時候有一個困難,那就是任何一個模塊都不是孤立的,比如進程的調度就設計到中斷處理、信號處理還有進程上下文的切換等等。作為一個初學者,想一下子把操作系統的整個運行過程都清晰地展現在腦海是不現實的。盡管如此,每個模塊還是有它所關注的焦點,我們所采取的策略是把整個操作系統分為幾個大模塊,比如:進程的管理、內存管理、文件系統等等。然后把這些大模塊進一步分解成一個個小模塊,比如進程的管理可以細分為進程的創建、進程的切換、系統調用的處理、信號的處理等等。在分析每一個模塊時,先把其他的模塊抽象化,千萬不要陷入其他模塊的細節當中,,也可以說這是一種各個擊破的方法,當你把每個小模塊的功能搞清楚后,到最后整個操作系統的運行過程就很清晰了!
*/
在上一篇博客中,我們提到當一個任務從系統調用處理函數返回之前會檢查是否需要進行進程切換。那么什么時候會發生進程的切換呢(任務調度)?當系統發生硬件中斷、系統調用或者時鍾中斷時,就有可能發生進程的切換。 下面我們以時鍾中斷為例來看看進程的切換是如何進行的。
在此之前,我們先要做一個說明,由於我們並沒有開始介紹進程的詳細知識,對進程的詳細介紹將放在進程的創建這一篇博客中(還沒開始寫O(∩_∩)O~),因此在這里我們先對進程做一個粗略的抽象:一個任務(就是進程)含有代碼段、數據段、堆棧段,還有一個任務狀態段TSS。這個任務狀態段TSS記錄當前任務的所有狀態信息,包括寄存器、系統參數等等。TSS段的描述符放在TR寄存器中(也就是說訪問TR就能訪問當前任務的TSS段了)。
假設此刻CPU正在執行進程1,我們知道:系統有一個時鍾頻率,每隔一段時間就會發生一次時鍾中斷,這個時間段我們稱為一個滴答。假設經過了一個滴答,系統發生時鍾中斷,此時時鍾中斷處理程序就會被自動調用(timer_interrupt),timer_interrupt定義在kernel/System_call.s中,如下圖所示:
同我們上一篇講的_system_call一樣,它首先會執行一些保護現場的工作,接着在第189行代碼中把_jiffies的值加1(_jiffies表示自系統啟動以來經過的滴答數),接下來第192-194的代碼將執行此次時鍾中斷的特權級CPL壓入堆棧,用來作為后面do_timer的參數,接下來開始執行do_timer,do_timer函數定義在Kernel/Sched.c中,這個函數的主要作用是將當前進程的用戶態執行時間或內核態執行時間加1,然后將當前進程的剩余時間片減1.
如果當前進程的時間片還有剩余,那么直接return返回繼續執行,接下來判斷當前任務的CPL是否是0,如果是0,說明當前任務是在內核態被中斷的,而Linux0.11中內核態是不能被搶占的,所以直接返回執行,如果不是0,則執行進程調度程序schedule()。接下來我們來分析schedule()這個函數,schedule函數同樣定義在Sched.c中,它里面包含下面兩段代碼:
這段代碼的作用是:遍歷任務數組,檢查它們的報警定時值,如果該值小於jiffies,說明該任務的alarm時間已經過了,那么就在它的信號位圖中置SIGALRM信號,表示向任務發送SIGALARM信號,然后將alarm清零,接下來檢查是不是還有別的未被阻塞的信號,如果有並且當前的進程狀態是可以被打斷的,那么把這個任務置為就緒態。
第124-142行的代碼重新遍歷整個任務數組,找出任務狀態處於TASK_RUNING並且時間片最長的那個任務。並調用swith_to()函數切換到那個任務。swith_to函數定義在include/Linux/Sched.h中。
switch_to是一段匯編代碼,下面來解釋一下這段代碼的含義:首先檢查要切換的任務是不是當前任務,如果是則直接退出。接下來把任務n(要切換去的任務)的TSS段放到_tmp.b中,然后把任務n放入_current中,把當前任務放入%ecx中切換出來,然后執行一個長跳轉到*&_tmp的位置(這是新任務的TSS地址處),此時CPU會把所有寄存器的內容保存到當前任務TR執行的TSS段中,然后把新任務的TSS段中的寄存器信息恢復到CPU的各個寄存器中,這樣系統就正式開始執行新的任務了。第178-180的代碼是判斷原任務是否使用過協處理器,如果沒有則直接結束。