摘要:本節將介紹進程的定義。進程作為構成系統的基本細胞,不僅是系統內部獨立啟動並執行實體,而且是獨立競爭資源的基底實體。瞭解進程的本質,對於理解、描述和設計作業系統有著極為重要的意義。瞭解進程的活動、狀態,也有利於編製複雜程式。
1.進程的基本概念
首先我們先看看進程的定義,進程是一個具有獨立功能的程式關於某個資料集合的一次可以並發執行的運行活動,是處於活動狀態的電腦程式。進程作為構成系統的基本細胞,不僅是系統內部獨立啟動並執行實體,而且是獨立競爭資源的基底實體。瞭解進程的本質,對於理解、描述和設計作業系統有著極為重要的意義。瞭解進程的活動、狀態,也有利於編製複雜程式。
1.1 進程狀態和狀態轉換
現在我們來看看,進程在生存周期中的各種狀態及狀態的轉換。下面是LINUX系統的進程狀態模型的各種狀態:
使用者狀態:進程在使用者狀態下啟動並執行狀態。
核心狀態:進程在核心狀態下啟動並執行狀態。
記憶體中就緒:進程沒有執行,但處於就緒狀態,只要核心調度它,就可以執行。
記憶體中睡眠:進程正在睡眠並且進程儲存在記憶體中,沒有被交換到SWAP裝置。
就緒且換出:進程處於就緒狀態,但是必須把它換入記憶體,核心才能再次調度它進行運行。
睡眠且換出:進程正在睡眠,且被換出記憶體。
被搶先:進程從核心狀態返回使用者狀態時,核心搶先於它,做了環境切換,調度了另一個進程。原先這個進程就處於被搶先狀態。
建立狀態:進程剛被建立。該進程存在,但既不是就緒狀態,也不是睡眠狀態。這個狀態是除了進程0以外的所有進程的最初狀態。
僵死狀態(zombie):進程調用exit結束,進程不再存在,但在進程表項中仍有紀錄,該紀錄可由父進程收集。
現在我們從進程的建立到退出來看看進程的狀態轉化。需要說明的是,進程在它的生命週期裡並不一定要經曆所有的狀態。
首先父進程通過系統調用fork來建立子進程,調用fork時,子進程首先處於建立態,fork調用為子進程配置好核心資料結構和子進程私人資料結構後,子進程就要進入就緒態3或5,即在記憶體中就緒,或者因為記憶體不夠,而導致在SWAP裝置中就緒。
假設進程在記憶體中就緒,這時子進程就可以被核心發送器調度上CPU運行。核心調度該進程進入核心狀態,再由核心狀態返回使用者狀態執行。該進程在使用者狀態運行一定時間後,又會被發送器所調度而進入核心狀態,由此轉入就緒態。有時進程在使用者狀態運行時,也會因為需要核心服務,使用系統調用而進入核心狀態,服務完畢,會由核心狀態轉回使用者狀態。要注意的是,進程在從核心狀態向使用者狀態返回時可能被搶佔,進入狀態7,這是由於有優先順序更高的進程急需使用 CPU,不能等到下一次調度時機,從而造成搶佔。
進程還會因為請求的資源不能得到滿足,進入睡眠狀態,直到它請求的資源被釋放,才會被核心喚醒而進入就緒態。如果進程在記憶體中睡眠時,記憶體不足,當進程睡眠時間達到一個閥值,進程會被SWAP出記憶體,使得進程在SWAP裝置上睡眠。這種狀況同樣可能發生在就緒的進程上。
進程調用exit系統調用,將使得進程進入核心狀態,執行exit調用,進入僵死狀態而結束。以上就是進程狀態轉換的簡單描述。
進程的上下文是由使用者級上下文、寄存器上下文以及系統級上下文組成。主要內容是該進程使用者空間內容、寄存器內容以及與該進程有關的核心資料結構。當系統收到一個中斷、執行系統調用或核心做環境切換時,就會儲存進程的上下文。一個進程是它的上下文中啟動並執行,若要調度進程,就要進行環境切換。核心在四種情況下允許發生環境切換:
當進程自己進入睡眠時;
當進程執行完系統調用要返回使用者狀態,但發現該進程不是最有資格啟動並執行進程時;
當核心完成中斷處理後要返回使用者狀態,但發現該進程不是最有資格啟動並執行進程時;
當進程退出(執行系統調用exit後)時。
有時核心要求必須終止當前的執行,立即從先前儲存的上下文處執行。這可由setjmp和longjmp實現,setjmp將儲存的上下文存入進程自身的資料空間(u區)中,並繼續在當前的上下文中執行,一旦碰到了longjmp,核心就從該進程的u區,取出先前儲存的上下文,並恢複該進程的上下文為原先儲存的。這時核心將使得進程從setjmp處執行,並給setjmp返回1。
進程因等待資源或其他原因,進入睡眠態是通過核心的sleep演算法。該演算法與本章後面要講到的sleep函數是兩個概念。演算法sleep記錄進程原先的處理機優先順序,置進程為睡眠態,將進程放入睡眠隊列,記錄睡眠的原因,給該進程進行環境切換。核心通過演算法wakeup來喚醒進程,如某資源被釋放,則喚醒所有因等待該資源而進入睡眠的進程。如果進程睡眠在一個可以接收非強制中斷訊號(signal)的層級上,則進程的睡眠可由非強制中斷訊號的到來而被喚醒。
1.2 進程式控制制
現在我們開始講述一下進程的控制,主要介紹核心對fork、exec、wait、exit的處理過程,為下一節學習這些調用打下概念上的基礎,並介紹系統啟動(boot)的過程以及進程init的作用。
在Linux系統中,使用者建立一個進程的唯一方法就是使用系統調用fork。核心為完成系統調用fork要進行幾步操作第一步,為新進程在進程表中分配一個表項。系統對一個使用者可以同時啟動並執行進程數是有限制的,對超級使用者沒有該限制,但也不能超過進程表的最大表項的數目。第二步,給子進程一個唯一的進程標識號(PID)。該進程標識號其實就是該表項在進程表中的索引號。第三步,複製一個父進程的進程表項的副本給子進程。核心初始化子進程的進程表項時,是從父進程處拷貝的。所以子進程擁有與父進程一樣的uid、euid、gid、用於計算優先權的nice值、目前的目錄、當前根、使用者檔案描述符表等。第四步,把與父進程相連的檔案表和索引節點表的引用數加1。這些檔案自動地與該子進程相連。第五步,核心為子進程建立使用者級上下文。核心為子進程的u區及輔助頁表分配記憶體,並複製父進程的區內容。這樣產生的是進程的靜態部分。第六步,產生進程的動態部分,核心複製父進程的內容相關的第一層,即寄存器上下文和核心棧,核心再為子進程虛設一個上下文層,這是為了子進程能“恢複”它的上下文。這時,該調用會對父進程返回子進程的pid,對子進程返回0。
Linux系統的系統調用exit,是進程用來終止執行時調用的。進程發出該調用,核心就會釋放該進程所佔的資源,釋放進程上下文所佔的記憶體空間,保留進程表項,將進程表項中紀錄進程狀態的欄位設為僵死狀態。核心在進程收到不可捕捉的訊號時,會從核心內部調用exit,使得進程退出。父進程通過 wait得到其子進程的進程表項中紀錄的計時資料,並釋放進程表項。最後,核心使得進程1(init進程)接收終止執行的進程的所有子進程。如果有子進程僵死,就向init進程發出一個SIGCHLD的非強制中斷訊號.
一個進程通過調用wait來與它的子進程同步,如果發出調用的進程沒有子進程則返回一個錯誤,如果找到一個僵死的子進程就取子進程的PID及退出時提供給父進程的參數。如果有子進程,但沒有僵死的子進程,發出調用的進程就睡眠在一個可中斷的層級上,直到收到一個子進程僵死(SIGCLD)的訊號或其他訊號。
進程式控制制的另一個主要內容就是對其他程式引用。該功能是通過系統調用exec來實現的,該調用將一個可執行檔程式檔案讀入,代替發出調用的進程執行。核心讀入程式檔案的本文,清除原先進程的資料區,清除原先使用者非強制中斷訊號處理函數的地址,當exec調用返回時,進程執行新的本文。
一個系統啟動的過程,也稱作是自舉的過程。該過程因機器的不同而有所差異。但該過程的目的對所有機器都相同:將作業系統裝入記憶體並開始執行。電腦先由硬體將引導塊的內容讀到記憶體並執行,自舉塊的程式將核心從檔案系統中裝入記憶體,並將控制轉入核心的入口,核心開始運行。核心首先初始化它的資料結構,並將根檔案系統安裝到根“/”,為進程0形成執行環境。設定好進程0的環境後,核心便作為進程0開始執行,並調用系統調用fork。因為這時進程0運行在核心狀態,所以新的進程也運行在核心狀態。新的進程(進程1)建立自己的使用者級上下文,設定並儲存好使用者寄存器上下文。這時,進程1就從核心狀態返回使用者狀態執行從核心拷貝的代碼(exec),並調用exec執行/sbin/init程式。進程1通常稱為初始化進程,它負責初始化新的進程。
進程init除了產生新的進程外,還負責一些使使用者在系統上註冊的進程。例如,進程init一般要產生一些getty的子進程來監視終端。如果一個終端被開啟,getty子進程就要求在這個終端上執行一個註冊的過程,當成功註冊後,執行一個shell程式,來使得使用者與系統互動。同時,進程init 執行系統調用wait來監視子進程的死亡,以及由於父進程的退出而產生的孤兒進程的移交。以上是系統啟動和進程init的一個粗略的模型。
1.3 進程調度的概念
Linux系統是一個分時系統,核心給每個進程分一個時間片,該進程的時間片用完就會調度另一個進程執行。LINUX系統上的發送器屬於多級反饋迴圈調度。該調度方法是,給一個進程分一個時間片,搶先一個運行超過時間片的進程,並把進程反饋到若干優先順序隊列中的一個隊列。進程在執行完之前,要經過這樣多次反饋迴圈。
進程調度分成兩個部分,一個是調度的時機,即什麼時候調度;一個是調度的演算法,即如何調度和調度哪個進程。我們先來看看調度的演算法,假設目前核心要求進行調度,發送器從“在記憶體中就緒”和“被搶先”狀態的進程中選擇一個優先權最高的進程,如果有若干優先權一樣高的進程,則在其中選擇等待時間最長的進程。切換進程上下文,繼續執行該進程。如果沒有選擇到進程,則不做操作,等待下一次調度時機的到來。
每一個進程都有一個用於調度的優先權域。進程的優先權由低到高粗略地分為使用者優先權和核心優先權。每種優先權有若干優先權值(優先數)與其對應。每個優先權都有一個邏輯上與其相連的進程隊列。進程從核心狀態返回使用者狀態時被搶先,從而得到使用者優先權。進程在核心演算法sleep中得到核心優先權。核心優先權高於使用者優先權,即核心優先權和使用者優先權之間存在一個閥值,所有使用者優先權低於該閥值,而核心優先權高於該閥值。核心優先權中又劃分為可中斷和不可中斷,即進程在收到一個非強制中斷訊號時,低核心優先權的進程可被喚醒,而有高核心優先權的進程繼續睡眠。
計算一個進程優先權的時機是:核心將一個優先權值賦給一個將進入睡眠的進程,這個優先權值是固定的,且與睡眠原因相聯絡;另一個時機是,時鐘處理常式每隔一定時間(如每隔1秒)調整使用者狀態下的所有進程的優先權,並使核心運行調度演算法。時鐘處理常式還根據一個衰減函數,每秒一次的調整每個進程的最近CPU使用時間。例如可按如下公式調整:
decay(CPU) = CPU/2;
再根據公式重新計算在“就緒”和“被搶先”狀態下的每個進程的優先權。
Priority = (“recent CPU usage”/constant) + (base priority) + (nice value);
其中constant是個系統常量(一般取值為“2”)。base priority值也是系統的一個常量,一般base priority取值為60。最後,nice的值是由進程發出nice調用時給出的值,這樣就可以使得使用者通過降低優先權而讓出一些執行時間。只有超級使用者才能指定提高優先權的nice值。