操作系統學習筆記_03_進程的概念與操作

1.進程的概念和狀態

進程的概念常常和程序糅合在一起,但兩者實際是不一樣的。首先,“進程”所包含的內容比“程序”豐富。“程序”這個概念可以用“一段代碼”來概括,這段代碼可能由不同的語言書寫,它有可能是可以直接執行的(比如機器碼),有可能是可以直接被解釋器執行的(比如MATLAB代碼),也有可能暫時不能被執行,需要編譯成機器碼之後才能執行(比如C++代碼)。項目是由代碼包裝而成的可執行文件。例如,C語言代碼在gcc環境下編譯形成項目由如下步驟組成:C語言代碼經過前端編譯之後形成彙編代碼,編譯器後端將彙編碼翻譯爲可執行的機器碼,然後機器碼依託其他文件(如庫文件等)包裝成爲可執行的項目。在項目被執行時,項目將被裝入內存。此時,項目的載體稱爲進程,執行進程就是對項目的內容進行解讀,並按照內容執行操作。一種狹義的說法稱進程爲“正在執行的一段代碼”,實際上進程信息的載體不僅包括執行代碼(文本段),還有程序計數器和處理器寄存器(保存當前活動狀態)、進程棧(包括臨時數據)、數據段(包括全局變量)和堆(進程運行期間動態分配的內存)等,這些信息在進程執行時都存儲在內存中。單獨從項目與代碼、進程對應的角度來看,項目可能由一段或多段代碼生成,也可能申請一個或多個進程來執行項目,他們之間都沒有一一對應的關係,所以上面的說法是不準確的。這種說法的可取之處在於指出了進程動態的特點:代碼和指令是靜態的操作,相當於工具;進程通過程序計數器取下一條執行的代碼或下一項執行的指令,相當於工具的使用者。

內存中的進程

在進程執行時,操作系統爲進程規定了不同的狀態,以便於操作系統對進程進行管理和維護。這些狀態包括新建(進程正在被創建),運行(正在執行指令),等待(等待某事件發生,比如收到I/O事件或其他事件的信號),就緒(等待處理器分配)和終止(執行完畢)。在同一時刻,進程只能處於同一狀態。因爲處理器的數目總是有限,每個處理器上每時每刻都只有一個進程處於運行狀態,而其他很多的進程都處於就緒或等待狀態。

進程的相關信息在操作系統中被保存在進程控制塊(下文簡稱PCB)中。PCB是一種存儲進程信息的數據結構,其內容包括進程狀態、程序計數器、CPU寄存器、CPU調度信息、內存管理信息、流水信息和I/O信息等。進程的PCB保存在內核中。當進程因系統調用而中斷執行時,其運行信息仍然由PCB保存。在進程恢復執行時,操作系統從PCB讀取進程原先的狀態,重新裝入進程。從這個角度看,PCB相當於進程在內核中的映射:進程在中斷時佔用資源的狀態原樣保存在用戶內存中,而執行狀態保存在內核的PCB中,體現出內核控制進程、進程支配資源的層級結構。

CPU在進程之間的切換
以P0爲例,CPU因爲任何原因而要暫停執行當前進程時,就將進程信息存入PCB,然後暫停。在開始執行一個進程時,通過加載PCB中的信息來獲取該進程的狀態,以P1爲例。

2.進程的操作

現代操作系統(以下以UNIX爲例)要求通過進程來訪問和利用資源,因此可以說所有的計算機操作都由進程完成,如文件操作、內存訪問與讀寫和流水信息記錄等。操作系統通常爲進程提供識別、創建、執行和終止等操作,通過特定的系統調用來實現。

進程識別

不同的進程具有不同的進程控制符(下簡稱PID),在需要識別特定進程時,比對PID即可。爲了實現這一點,操作系統在創建進程時賦予進程不同(unique)的PID,並通過系統調用管理:系統調用getpid()可用於獲得進程的PID.

進程創建

進程的創建和管理通過維護進程樹實現。在UNIX中,PID爲1的init進程在裝入系統時就產生並開始執行,其任務是爲用戶創建進程。正在執行的進程通過系統調用創建新進程,前者和後者分別稱爲父進程和子進程。新創建的進程還可以創建其他子進程,所以可以用進程樹表示進程間的創建關係;init進程就是這棵樹的根節點,是所有用戶進程的根進程。然而,進程在內核存儲中的實際組織方式並不是樹,而是雙向鏈表,其中每個進程都存儲有自身父進程和全體子進程的PID.

用於創建進程的系統調用是fork().由fork()產生的子進程由原來進程的地址空間的副本組成,前者完全複製父進程的程序計數器、文件狀態(進程打開的文件在內核結點中由列表存儲,所以狀態可以克隆)和程序代碼,並獲得獨享的內存空間,將父進程在用戶內存的資源完全拷貝過來。在內核中,子進程結點完全從父進程克隆過來,只改變PID、運行時間和繼承關係。

除此之外,還需要系統調用exec*()輔助管理進程。exec*()是一族系統調用的總稱,它們將當前進程映像替換成新的程序文件,所以exec*()不產生新進程,而是造成原進程內容的變化。具體而言,在進程調用exec*()時,進程轉爲執行參數所指示的程序。此時原來的程序代碼不復存在(被新的指令代碼替換),進程改爲執行新的指令;原來的執行信息不復保留,內存和寄存器資源被重置。進程只保留原來的PID、繼承關係和運行時間等信息,在新的指令執行完畢後直接終止。

此處額外說明一點:系統調用的設計理念就是儘量少進行硬件操作(如訪問內存),因爲所需時間長。所以上面介紹的fork()和exec*()在內核中的實際操作就是本着這樣的理念設計的。

在系統實際運行時,fork()和exec*()經常搭配使用,以實現父進程中包含子進程的效果。爲了實現上述操作,需要父進程執行wait()或waitpid()系統調用以暫停執行,在子進程通過exit()終止時方被喚醒,繼續執行。具體而言,程序使用一個針對fork()的條件判斷(父子進程中fork()的返回值不同)來使父子進程執行不同指令。父進程執行到wait()後建立一個信號接收器,之後保存狀態進入等待。子進程在執行到exit()後釋放所有資源,只保留最少的信息(PID、運行時間和退出信息),併發送SIGCHLD信號給父進程。父進程在接受信號後被喚醒(如果是由waitpid()產生的接收器則只接受特定子進程的信號),接收器默認將信號移除,並銷燬發信的子進程。從用戶的視角看,這種操作使得父進程在創建子進程後的某時刻掛起,待後者結束後按原樣執行。另外,UNIX中默認先運行父進程,所以這種操作也提供了調換執行順序的選項。如果子進程發送信號早於父進程建立接收器,則信號將被保留,待接收器建立之後立即被接收並執行銷燬。

如果父進程wait()早於子進程結束,父進程就一直wait(),至子進程終止,父進程接收到信號,kill子進程

如果父進程wait()晚於子進程結束,子進程終止之後保留信號,父進程wait()時立即接收到信號,kill子進程

在一種特殊情況下,如果在子進程運行結束前父進程已經終止,子進程就會變成孤兒進程。這樣做會使得子進程和進程樹失去聯繫,使得操作系統無法控制其終止所帶來的影響。UNIX爲此提供了重定父址的操作,在每個進程終結時將其所有子進程樹的根進程確定爲init進程的子進程,即選擇init進程作爲這些孤兒進程的繼父。init進程週期性地執行接收信號操作,保證這些殭屍進程(執行完的子進程)能及時被銷燬。

參考文獻:

[1] Abraham Silberschatz. 操作系統概念:Java實現:第7版:翻譯版. 高等教育出版社, 2010.1;

[2] 韓其睿. 操作系統原理. 清華大學出版社, 2013.8.

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