程序與執行緒(二)——程序的管理、建立與銷燬
接上回:
我們介紹了程序的由來,程序的概念,程序的組成部分和它在執行過程中的狀態。我們說程序最重要的部分是程序控制塊PCB,作業系統通過PCB來管理各個程序有條不紊的在我們的機器中執行的。那麼作業系統是怎麼樣管理這麼多程序的呢?程序是怎麼樣建立、又是怎麼樣銷燬的呢?
一.PCB的組織方式
一個系統中通常可有數十個甚至數百個乃至數千個PCB,為了能對他們加以有效的管理,應當用適合當的方式將這些組織起來。要回答這個問題,就要從PCB的組織方式來講起。
我們說程式等於資料結構加演算法,這句話並不是空穴來風,這裡將涉及到我們資料結構的內容。
(1) 線性方式:
將系統中所有的PCB 都組織加一張線性表中將該表的手指放在記憶體當中的一個專用區域中。
(2) 連結方式:
一般使用連結串列的資料結構,為什麼:因為程序在執行的過程是動態的執行過程,在管理的時候程序會一會建立一會兒結束,在組織裡面可以動態的插入和刪除,用連結串列的形式可以更好的實現動態的刪除和插入功能。
索引的話動態的刪除和插入開銷會大一點,目前來說才去基於連結串列的的組織方式。如果一開始程序比較固定,不會頻繁的建立和刪除的話,採取索引的方式也是一種快捷的組織方式。 所以特殊的作業系統和通用的作業系統會有不同的組織方式。
(3) 索引方式
系統根據所有程序狀態的不同,建立幾張索引表。例如,就緒索引表,阻塞索引表等。並把個索引表在記憶體的首地址記錄在記憶體的一些專用表單元中。每個索引表的表目中,記錄具有相應狀態的某個PCB在PCB表中的地址。
二. 狀態佇列
我們可以看出:
1. 作業系統來維護一組佇列,用於表示系統當中所有程序的當前狀態,
2. 不同的狀態,分別用不同的佇列來表示。就緒佇列各種型別的阻塞佇列。
3. 每個程序都根據他的狀態加入到相應的隊列當中。當程序的狀態發生變化時,它的PCB從一個狀態脫離出來,加入到另外一個佇列。
如圖所示:
我們還可以看到:多個優先佇列,分優先順序
例如:根據阻塞等待的不同的事件,會排不同的佇列:事件1滿足一個或者多個程序,程序就會變為就緒佇列。
如果事件1只能滿足一個程序的話,那麼我們只能把佇列中的一個程序從阻塞態變為就緒態。如果事件1產生之後,所有等待事件1的這些程序都可以得到滿足,我們就需要把所有的程序都從阻塞態轉變為就緒態,整個佇列裡的程序就會誇到就緒佇列裡去。
三. 建立程序,載入和執行程序
首先有這麼一段程式碼。(終於看到程式碼了)
我們將講解以下幾個步驟:
1.建立fork() 父程序建立子程序
2.載入:EXEC()
3.等待:wait()
初始狀態:
父程序的空間
執行Fork()後:新的子程序把父程序程式碼和資料都複製了一份,同時建立了一個新的屬於自己的PID(128)。如圖所示:
執行EXEC()後:
系統呼叫EXEC()載入執行新的程式,取代當前執行的程序,覆蓋所有的程式碼和資料,都變成新的程式裡面的內容。但是請注意:新的PID(128)沒有變。
這個地方有點繞,多看幾遍哪裡沒變化,哪裡有變化。
我們再來看一遍記憶體佈局圖:
單獨的父程序
fock()+exec()後:建立新的地址空間,新的程式碼段、新的程序:
一、fork子程序的地址空間完全是父程序的
二,但是執行完exec的時候可以看到:1.PCB裡面資訊變化 2. 使用者態記憶體空間變化,程式碼段完全被新的程式替換。
calc_mian開始執行,也就是說:當子程序執行exec時候,整個程式的控制流發生了變化。
注意:Exec載入不同的程式,來執行新的程式,執行程序指定不同的控制流,這樣可以使得在作業系統裡面可以執行不同的應用程式,很好的一個方法,提供了一個設計思路:建立一個程序,繼承父程序的所有的程式碼和資料,還是說完成一個新的程式的工作。這個可以根據應用程式的需求來做相應的處理。注意執行EXEC的時候,程序本身的程式碼段、資料段、堆疊都會被覆蓋。
作為了解:
Fork因為複製程式碼佔用很多的系統開銷,如果fork效率高,可以提高作業系統的效率。fork把父程序的地址空間完全複製一份到子程序的空間中來,有一個記憶體的大量的拷貝(程式碼段,資料段)。但是執行exec的時候,剛剛的拷貝全都是沒有用的,因為要載入一個新的程式,要把程式碼段和資料段重新覆蓋掉,前面做的fork工作其實是多餘的。
有什麼辦法優化呢?
1.虛fork——vfork,複製的時候只是複製了一小部分的內容,絕大部分的內容沒複製。——變成了兩個(fork、vfork)
2.作業系統各個子系統之間 相互支援相互幫助,我們通過虛擬記憶體的管理,就可以出現一個高效的fork實現機制——Copy on Write技術。寫的時候在進行復制。——當父進行建立子程序的時候 ,在實際的複製的時候,沒有把整個地址空間真實的複製,只複製了父程序所需要的元資料(頁表),它們指向了同一塊地址空間。當父程序或者子程序對某一個地址單元進行寫操作的時候,會觸發一個異常,使不論是父程序還是子程序, 要把這個觸發異常的這個頁複製成兩份 ,讓父程序和子程序擁有兩個個不同的地址。這種方式,實現按需寫的複製,如果只是只讀,確實沒有必要複製,只有當寫的時候,我們需要子程序和父程序需要有不同的頁————很有效率。
不管後面是否執行EXEC(),這個系統呼叫,我們的fork:第一,還是和之前的語義是一樣的,能完全建立子程序(而且執行效率很高,因為只複製了地址空間管理相關的所需要的那個源資料的頁表等)。但是是根據是否完成寫操作來決定是否要去複製。COW是我們程序管理和記憶體管理一個有效的相互支撐的一直機制。
四.等待和終止程序
wait()系統呼叫是被父程序用來等待子程序的結束,一個子程序向父程序返回一個值,父程序必須接受這個值並處理。
為什麼要讓父程序等?而不是直接結束?
當程序執行完畢退出後,幾乎所有資源都回收到OS中。但有個資源很難回收,就是PCB,PCB是代表程序存在的唯一標識,作業系統要依據PCB程序執行回收。這個功能由父程序完成。子程序exit()和父程序wait()匹配,父程序完成把子程序PCB釋放:
Wait()使父程序睡眠,當子程序呼叫exit()時作業系統解鎖父程序,將通過exit傳遞得到的返回值作為wait呼叫的一個結果(連同子程序的pid一起)。關閉所有開啟的檔案和連線,釋放記憶體,釋放大部分支援程序的OS結構,檢查父程序是否存活。
如果父程序存活,它保留exit結果的值直到父程序需要它,進入殭屍狀態。
還有一種情況:如果父程序掛了,子程序釋放所有的資料結構,這個程序死亡。主終程序root 或者根程序會定期的掃描PCB程序控制塊的連結串列,看是否有程序處於殭屍態的狀態,如果有程序處於殭屍狀態, 就代父程序來完成回收操作。
什麼是殭屍狀態:
就是呼叫了子程序EXIT但父程序還沒有執行到wait返回的時候。子程序將死,還沒死。無法正常工作,只是等待被父程序回收。
現在,我們把fork、exit、wait加到程序狀態圖中。留一個問題,exec()應該在哪呢?