一個操作系統的實現(8):進程

本文是爲了弄清一下幾個問題:

1)什麼是進程?

2)進程由那些部分組成?

3)如何創建一個進程?

4)如何確保進程間互不干擾?

5)進程切換做了哪些工作?

6)進程調度是怎麼實現的?

7)什麼是系統調用?


1)什麼是進程?

下面來先來回答第一個問題:什麼是進程?百度百科上給出了定義,狹義上講進程就是一段程序的執行過程,從廣義上講進程是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動。舉個例子來說,你準備好菜譜,和燒菜的材料,然後把材料按照菜譜燒菜一道菜的過程就是一個進程。而那菜譜和材料就好比是程序裏的代碼和數據。程序是靜態的,而進程是程序執行的動態過程。

2)進程由那些部分組成?

那麼一個進程由那些部分組成呢?這就是上面提出的第二個問題。一般來講,進程由4個部分組成:進程控制塊、代碼、數據、堆棧。其中進程控制塊又稱進程表,它的作用是保存進程的狀態,它也是進程的唯一標誌。那進程表裏是什麼內容呢,來看下進程表結構的定義:

typedef struct s_stackframe {                                                                                           
        u32     gs;             /* \                                    */
        u32     fs;             /* |                                    */
        u32     es;             /* |                                    */
        u32     ds;             /* |                                    */
        u32     edi;            /* |                                    */
        u32     esi;            /* | pushed by save()                   */
        u32     ebp;            /* |                                    */
        u32     kernel_esp;     /* <- 'popad' will ignore it            */
        u32     ebx;            /* |                                    */
        u32     edx;            /* |                                    */
        u32     ecx;            /* |                                    */
        u32     eax;            /* /                                    */
        u32     retaddr;        /* return addr for kernel.asm::save()   */
        u32     eip;            /* \                                    */
        u32     cs;             /* |                                    */
        u32     eflags;         /* | pushed by CPU during interrupt     */
        u32     esp;            /* |                                    */
        u32     ss;             /* /                                    */
}STACK_FRAME;                                                                                                           


typedef struct s_proc {                                                                                                 
        STACK_FRAME regs;          /* process registers saved in stack frame */

        u16 ldt_sel;               /* gdt selector giving ldt base and limit */
        DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */
        u32 pid;                   /* process id passed in from MM */
        char p_name[16];           /* name of the process */
}PROCESS; 

可以看到PROCESS結構體中,包含了5個成員,他們分別是保存進程相關寄存器的棧幀、局部描述符表選擇子、局部描述符表數組、pid、進程名。其中最重要的是保存寄存器的棧幀和局部描述符表,棧幀結構裏保存中進程被中斷時各個寄存器中的狀態,而局部描述符表選擇子對應GDT中的一個描述符,根據它來找到局部描述符表數組中對應的LDT(該內容在之前文章中介紹過)。

3)如何創建一個進程?

接下來看下第三個問題:如何創建一個進程?首先看下面這張圖,它展示了我們第一個進程的啓動過程:


當我們再添加進程是,需要考慮的要素無外乎4點:進程體、進程表、GDT、TSS。


4)如何確保進程間互不干擾?

這個問題可以從兩個方面來講,一個是內存管理,另一個是進程表切換。先將內存管理,這點之前文章已經分析過了。cpu在執行代碼的時候,全局或局部描述符表,分別尋找內核態代碼和用戶態代碼。而對於進程來講,他們擁有各自的局部描述符表,也就是他們都共享一份內核態代碼但是用戶態代碼都是獨立的。並且這些代碼經過分頁機制之後被拷貝到內存,這個內存地址由分頁的映射關係決定,而這個映射關係是每個進程都不一樣的。相當與內存管理中對進程進行了兩層隔離。

另一個方面就是進程表切換,在多進程環境中,爲什麼進程切換但是有不會相互干擾呢。這是因爲在進程調度來臨,也就是時鐘中斷來臨時,把當前cpu各個寄存器的值都保存到了進程表中,當要切換回該進程時,再把進程表中的值賦值給cpu各個寄存器。所有進程都這樣操作,就確保了進程切換但不會互相干擾。

5)進程切換做了哪些工作?
爲了實現進程切換,我們首先要考慮的一個問題是:如何保存非活動進程的狀態。
進程要佔用內存空間,裏面存放的是代碼和數據。內存空間不用去管它,放在那裏就好。只要系統不將其標記爲可用內存就行。
那剩下的問題就是堆棧和寄存器。寄存器的值用一個“pushad”就可以全部壓入棧中保存,可是問題就來了:ESP寄存器是棧指針,它的值該如何保存?
Intel CPU規定,在使用pushad時,ESP的值爲執行指令時ESP寄存器的值。因此,使用pushad保存的是原來指針的位置。

由於進程切換是運行在內核級的操作,所以它的特權級爲0。因此這裏又涉及到了一次特權級轉換。假設進程運行在特權級3,切換進程時,TSS會自動將SS:ESP切換到0特權級使用的棧,因此我們要從3特權級使用的棧中取出SS:ESP的值,連同其它寄存器儲存在某一個地方,這個地方就叫進程表。雖然進程表是一個結構體,而不是棧,但是爲了將寄存器壓入進程表,我們還需要將ESP指向進程表。

寄存器的值保存完畢後,我們再讓ESP指向內核使用的0特權級棧,並開始進程調度。

下一個問題:我們應當在什麼時候決定將CPU的控制權交給另一個進程?
一般來說,多個進程共享一個CPU的話,每個進程執行的時間應該是一樣的(後面再考慮進程優先級的問題)。在相同的時間內反覆發生的事件,也許你已經想到了,就是CPU的時鐘中斷。在中斷的時候,我們將寄存器和堆棧保存到進程表。這個工作叫做保護現場。而調度完畢,準備執行下一個進程前,要恢復各個寄存器的值,這個過程叫做恢復現場。爲了準備下一次進程切換,TSS中SS0(內核特權級的棧指針)必須指向下一個進程的進程表。這個工作要在從中斷返回前一刻執行。返回中斷的時候,iretd實際上是從棧中讀取返回時的CS、EIP、SS、ESP的,由於進程表特殊的結構,幾個寄存器的值正好會被設定成下一個進程應有的值,從而實現將CPU的控制權交給下一個進程的操作。
因此,進程切換的步驟如下:
發生中斷
將ESP保存並指向進程表
將保存的ESP及其它寄存器的值保存到進程表
將ESP指向內核棧
進程調度,選擇下一個準備執行的進程
將ESP指向下一個進程的進程表
中斷返回
需要注意的問題

如果在中斷髮生過程中又發生中斷會怎麼樣?這種情況叫做中斷重入。經過書中的試驗可以得知,出現這樣的情況,進程調度模塊就會進入死循環,無法回到進程,而是不斷遞歸調用。隨着時間的推移,堆棧會溢出,於是意想不到的事情就會發生。
解決辦法是:設一個全局變量,如果中斷處理程序執行時檢測到該變量已置位,則跳過處理的步驟;否則將其置位。在處理結束後,再將其復位即可。

6)進程調度如何實現?

進程調度涉及的是如何在不同優先級的進程之間分配CPU時間的問題。如果所有進程都是同一個優先級,則不需要調度,中斷的時候直接跳到下一個進程即可。正因爲有了優先級的存在,纔有了進程調度模塊。爲了實現優先級,我們給每一個進程加一個變量ticks。當時鍾中斷髮生時,就將ticks減一。當ticks小於0時,將控制權交給下一個進程,該進程不再有執行的機會,直到所有進程的ticks都爲0爲止。然後將ticks恢復爲各自優先級的數值,並開始下一輪循環。原先的進程調度可以視爲每個進程的優先級都爲1。由於優先級的數值可以很大,比如10、15,所以有必要將時鐘中斷的頻率調高一些(方法參見書中原文)。這樣一來,就打破了進程執行時間的對稱性,優先級得以體現。

7)什麼是系統調用?
系統調用跟進程調度和進程切換沒有太大的關係,在這裏僅僅提一句。
比如,一個進程想要知道自己ticks的值,怎麼辦呢?由於特權級的限制,它不能自己去訪問保存ticks的內存,所以只有求助於系統內核。這就需要內核給進程開放一個“接口”,也就是面向用戶進程的函數,這就是系統調用。
對於系統調用的實現,Minix設計了一套消息機制,需要系統支持的功能全部用這套機制實現;而Linux則直接由內核實現所有的系統調用。因此Minix是微內核,Linux是宏內核。







發佈了46 篇原創文章 · 獲贊 11 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章