標籤:linux編程 進程 fork 寫時複製 父子進程
進程 VS. 程式
什麼是程式?
程式是完成特定任務的一系列指令集合。
什麼是進程?
[1]從使用者的角度來看:進程是程式的一次執行過程
[2]從作業系統的核心來看:進程是作業系統分配的記憶體、CPU時間片等資源的基本單位。
[3]進程是資源分派的最小單位
[4]每一個進程都有自己獨立的地址空間與執行狀態。
[5]像UNIX這樣的多任務作業系統能夠讓許多程式同時運行,每一個運行著的程式就構成了一個進程
進程資料結構
進程由三部分組成:PCB、程式段和資料區段。
進程式控制制塊PCB:用於描述進程情況及控制進程運行所需的全部資訊。
程式碼片段:是進程中能被進程發送器在CPU上執行的程式碼段。
資料區段:一個進程的資料區段,可以是進程對應的程式加工處理的未經處理資料,也可以是程式執行後產生的中間或最終資料
進程和程式的區別
進程是動態(進程存在的唯一標誌:PCB, CPU通過PCB來控制進程),程式是靜態
進程的生命週期是相對短暫的,而程式是永久的。
一個進程只能對應一個程式,一個程式可以對應多個進程。
進程三態
進程因建立而就緒,因調度而執行;因時間片用完而重新就緒;
執行中因I/O請求而阻塞;
I/O完成而就緒
注意:阻塞以後不能直接執行,必須進入就緒狀態。
Linux核心中的進程狀態
運行狀態(TASK_RUNNING)
可中斷睡眠狀態(TASK_INTERRUPTIBLE)
不可中斷睡眠狀態(TASK_UNINTERRUPTIBLE)
暫停狀態(TASK_STOPPED)
僵死狀態(TASK_ZOMBIE)
進程調度
進程調度的任務
儲存處理機的現場資訊
按某種演算法選取進程
把處理器分配給進程
進程調度演算法
1.先來先服務演算法
2.短進程優先演算法
3.時間片輪轉演算法
4.優先順序調度演算法
5.多級反饋隊列調度演算法
進程編程相關術語
進程標誌:
每個進程都會分配到一個獨一無二的數字編號,我們稱之為“進程標識”(process identifier),或者就直接叫它PID.
是一個正整數,取值範圍從2到32768
當一個進程被啟動時,它會順序挑選下一個未使用的編號數字做為自己的PID
1號進程是特殊進程init
0號進程空閑進程
關於0,1的解釋:
進程0:Linux引導中建立的第一個進程,完成載入系統後,演變為進程調度、交換及儲存管理進程;
進程1:init 進程,由0進程建立,完成系統的初始化. 是系統中所有其它使用者進程的祖先進程;
進程建立
不同的作業系統所提供的進程建立原語的名稱和格式不盡相同,但執行建立進程原語後,作業系統所做的工作卻大致相同,都包括以下幾點:
(1)給新建立的進程分配一個內部標識,在核心中建立進程結構。
(2)複製父進程的環境
(3)為進程分配資源, 包括進程映像所需要的所有元素(程式、資料、使用者棧等),
(4)複製父進程地址空間的內容到該進程地址空間中。
(5)置該進程的狀態為就緒,插入就緒隊列。
進程撤銷
進程終止時作業系統做以下工作:
(1)關閉非強制中斷:因為進程即將終止而不再處理任何非強制中斷訊號;
(2)回收資源:釋放進程分配的所有資源,如關閉所有已開啟檔案,釋放進程相應的資料結構等;
(3)寫記帳資訊:將進程在運行過程中所產生的記帳資料(其中包括進程運行時的各種統計資訊)記錄到一個全域記帳檔案中;
(4)置該進程為僵死狀態:向父進程發送子進程死的非強制中斷訊號,將終止資訊status送到指定的儲存單元中;
(5)轉進程調度:因為此時CPU已經被釋放,需要由進程調度進行CPU再分配。
fork系統調用
複製一個進程映象
使用fork函數得到的子進程從父進程的繼承了整個進程的地址空間,包括:進程上下文、進程堆棧、記憶體資訊、開啟的檔案描述符、訊號控制設定、進程優先順序、進程組號、當前工作目錄、根目錄、資源限制、控制終端等。
子進程與父進程的區別:
1、父進程設定的鎖,子進程不繼承
2、各自的進程ID: 父子進程ID不同
3、子進程的未決警告被清除;
4、子進程的未決訊號集設定為空白集;
fork系統調用
#include <unistd.h>pid_t fork(void);
建立一個子進程
傳回值:
如果成功建立一個子進程,對於父進程來說返回子進程ID
如果成功建立一個子進程,對於子進程來說傳回值為0
如果為-1表示建立失敗
怎樣理解fork函數一次調用,二次返回?
問題的本質是:兩次返回,是在各自的進程空間中返回的。
子進程和父進程各有自己的記憶體空間 (fork:程式碼片段、資料區段、堆棧段、PCB進程式控制制塊的copy)。
//樣本: 父子進程中資料的關係(其實基本沒關係)int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); int count = 10; pid_t pid = fork(); if (pid == -1) err_exit("fork error"); else if (pid == 0) //子進程 { ++ count; cout << "In child: pid = " << getpid() << ", ppid = " << getppid() << endl; cout << "count = " << count << endl; } else if (pid > 0) //父進程 { ++ count; cout << "In parent: pid = " << getpid() << ", child pid = " << pid << endl; cout << "count = " << count << endl; } exit(0);}
//深入理解: Hello World 為什麼會列印8次int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); fork(); fork(); fork(); cout << "Hello World" << endl; exit(0);}
//樣本: 產生N個子進程int main(int argc, char *argv[]){ signal(SIGCHLD, SIG_IGN); int processCount; cin >> processCount; for (int i = 0; i < processCount; ++i) { pid_t pid = fork(); if (pid < 0) err_exit("fork error"); else if (pid == 0) { cout << "Child ..." << endl; exit(0); } } exit(0);}寫時複製(copy on write)
COW初窺:
在Linux程式中,fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,Linux中引入了“寫時複製“技術,也就是只有進程空間的各段的內容要發生變化時,才會將父進程的內容複寫一份給子進程。
那麼子進程的物理空間沒有代碼,怎麼去取指令執行exec系統調用呢?
在fork之後exec之前兩個進程用的是相同的物理空間(記憶體區),子進程的程式碼片段、資料區段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間,如果不是因為exec,核心會給子進程的資料區段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而程式碼片段繼續共用父進程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由於兩者執行的代碼不同,子進程的程式碼片段也會分配單獨的物理空間。
在網上看到還有個細節問題就是,fork之後核心會通過將子進程放在隊列的前面,以讓子進程先執行,以免父進程執行導致寫時複製,而後子進程執行exec系統調用,因無意義的複製而造成效率的下降。
COW詳述:
現在有一個父進程P1,這是一個主體,那麼它是有靈魂也就身體的。現在在其虛擬位址空間(有相應的資料結構表示)上有:本文段,資料區段,堆,棧這四個部分,相應的,核心要為這四個部分分配各自的物理塊。即:本文段塊,資料區段塊,堆塊,棧塊。
1. 現在P1用fork()函數為進程建立一個子進程P2,
核心:
(1)複製P1的本文段,資料區段,堆,棧這四個部分,注意是其內容相同。
(2)為這四個部分分配物理塊,P2的:本文段->P1的本文段的物理塊,其實就是不為P2分配本文段塊,讓P2的本文段指向P1的本文段塊,資料區段->P2自己的資料區段塊(為其分配對應的塊),堆->P2自己的堆塊,棧->P2自己的棧塊。
如所示:從左至右大的方向箭頭表示複製內容。
2.寫時複製技術:核心只為新產生的子進程建立虛擬空間結構,它們複製於來自父進程的虛擬空間結構,但是不為這些段分配實體記憶體,它們共用父進程的物理空間,當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間。
3. vfork():這個做法更加火爆,核心連子進程的虛擬位址空間結構也不建立了,直接共用了父進程的虛擬空間,當然了,這種做法就順水推舟的共用了父進程的物理空間
小結: 進程是一個主體,那麼它就有靈魂與身體,系統必須為實現它建立相應的實體, 靈魂實體與物理實體。這兩者在系統中都有相應的資料結構表示,物理實體更是體現了它的物理意義。
傳統的fork()系統調用直接把所有的資源複製給新建立的進程。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共用,更糟的情況是,如果新進程打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以延遲甚至免除拷貝資料的技術。核心此時並不複製整個進程地址空間,而是讓父進程和子進程共用同一個拷貝。只有在需要寫入的時候,資料才會被複製,從而使各個進程擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以唯讀方式共用。這種技術使地址空間上的頁的拷貝被延遲到實際發生寫入的時候。在頁根本不會被寫入的情況下{舉例來說:fork()後立即調用exec()}它們就無需複製了。fork()的實際開銷就是複製父進程的頁表以及給子進程建立惟一的進程描述符。在一般情況下,進程建立後都會馬上運行一個可執行檔檔案,這種最佳化可以避免拷貝大量根本就不會被使用的資料(地址空間裡常常包含數十兆的資料)。由於Unix強調進程快速執行的能力,所以這個最佳化是很重要的。這裡補充一點:Linux COW與exec沒有必然聯絡
PS:實際上COW技術不僅僅在Linux進程上有應用,其他例如C++的String在有的IDE環境下也支援COW技術,即例如:
string str1 = "hello world";string str2 = str1;
之後執行代碼:
str1[1]=‘q‘;str2[1]=‘w‘;
在開始的兩個語句後,str1和str2存放資料的地址是一樣的,而在修改內容後,str1的地址發生了變化,而str2的地址還是原來的,這就是C++中的COW技術的應用;
[附]-查看系統能夠支援的最大進程數
cat /proc/sys/kernel/pid_max
Linux進程實踐(1) --Linux進程編程概述