啟動新進程(system函數)
system()函數可以啟動一個新的進程。
int system (const char *string )
這個函數的效果就相當於執行sh –c string。
一般來說,使用system函數遠非啟動其他進程的理想手段,因為它必須用一個shell來啟動需要的程序。這樣對shell的安裝情況,以及shell的版本依賴性很大。
system函數的特點:
建立獨立進程,擁有獨立的代碼空間,內存空間
等待新的進程執行完畢,system才返回。(阻塞)
替換進程映像(exec函數)
exec函數可以用來替換進程映像。執行exec系列函數后,原來的進程將不再執行,新的進程的PID、PPID和nice值與原先的完全一樣。其實執行exec系列函數所發生的一切就是,運行中的程序開始執行exec調用中指定的新的可執行文件中的代碼。
exec函數的特點:
當進程調用一種exec函數時,源進程完全由新程序代換,而新程序則從其main函數開始執行。因為調用exec並不創建新進程,所以前后的進程ID並未改變。exec只是用另一個新程序替換了當前進程的正文、數據、堆和棧段。特別地,在原進程中已經打開的文件描述符,在新進程中仍將保持打開,除非它們的“執行時關閉標志”(close on exec flag)被置位。任何在原進程中已打開的目錄流都將在新進程中被關閉。
復制進程映像(fork函數)
fork函數
頭文件
- #include<unistd.h>
- #include<sys/types.h>
函數原型
- pid_t fork( void);
返回值:
若成功調用一次則返回兩個值,子進程返回0,父進程返回子進程ID;否則,出錯返回-1
關於fork函數的作用,《Linux程序設計》中是這樣解釋的:
我們可以通過調用fork創建一個新進程。這個系統調用復制當前進程,在進程表中新建一個新的表項,新表項中的許多屬性與當前進程是相同的。新進程幾乎與元進程一模一樣,執行的代碼也完全相同,但是新進程有自己的數據空間、環境和文件描述符。
這個解釋其實過於籠統,很多細節問題都沒有說。下面就簡單說一下調用fork時發生的一些細節問題。或者叫fork函數的特點:
首先,現在的UNIX系統和Linux系統都采用寫時復制技術(COW:Copy On Write)。使用這種技術,當調用fork函數時,新的進程只是擁有自己的虛擬內存空間,而沒有自己的物理內存空間。新進程共享源進程的物理內存空間。而且新內存的虛擬內存空間幾乎就是源進程虛擬內存空間的一個復制。
我們知道,進程空間可以簡單地分為程序段(正文段)、數據段、堆和棧四部分(簡單這樣理解)。采用寫時復制的fork函數,當執行完fork后的一定時間內,新的進程(子進程)和源進程的進程空間關系如下圖:
如上圖,fork執行時,Linux內核會為新的進程P2創建一個虛擬內存空間,而新的虛擬空間中的內容是對P1虛擬內存空間中的內容的一個拷貝。而P2和P1共享原來P1的物理內存空間。
當然要理解“寫時復制”中,上圖中所展示的狀態是會發生變化的。什么時候回發生變化呢?就是,父子兩個進程中任意一個進程對數據段、棧區、堆區進行寫操作時,上圖中的狀態就會被打破,這個時候就會發生物理內存的復制,這也就是叫“寫時復制”的原因。發生的狀態轉變如下:
我們發現,P2有了屬於自己的物理內存空間。值得注意的是,各個段之間發生的變化應當是獨立的,也就是說,如果只有數據段發生了寫操作那么就只有數據段進行寫時復制。而堆、棧區域依然是父子進程共享。還有一個需要注意的是,正文段(程序段)不會發生寫時復制,這是因為通常情況下程序段是只讀的。子進程和父進程從fork之后,基本上就是獨立運行,互不影響了。
此外需要特別注意的是,父子進程的文件描述符表也會發生寫時復制。
還有一個叫vfork的函數,這個做法更加火爆,內核連子進程的虛擬地址空間結構也不創建了,直接共享了父進程的虛擬空間,當然了,這種做法就順水推舟的共享了父進程的物理空間
system()、exec()、fork()函數比較
首先比較一下exec()函數和fork()。這兩個函數一個是換葯不換湯(execl函數),另一個是換湯不換葯(fork函數)。那么什么是湯、什么又是葯呢?我們知道進程是個很復雜的東西。從task_struct 結構體的代碼量上就可以看出來(task_struct是Linux內核中用來描述進程的一個結構體,這個結構體光代碼貌似就有好幾屏)。我們可以把進程的PID、PPID和nice值等看作是湯,而把進程空間(簡單理解就是正文段、數據段、堆、棧等)看作是葯。
exec()函數是換葯不換湯,就是說執行exec函數后,並沒有產生新的進程,也就是湯還是那些湯,進程的PID、PPID和nice值等沒有發生變化。但是exec()函數卻將葯換了,也就是將進程空間換掉了,新的進程空間是為了執行新的程序所准備的,所以新的進程空間與原進程空間並沒有什么關系。
fork()函數是換湯不換葯,意思是執行fork()函數后,產生了新的進程,新的進程的PID、PPID與原來原來的進程不同,說明父子進程是兩個不同的進程,但是fork並沒有把葯換掉,而是將葯復制了一份給子進程。fork剛執行后的一段時間內,父子進程有着相同的狀態(進程空間中的東西都一樣,因為fork采用“寫時復制”,一開始父子進程共享物理內存空間)。但是一旦父子進程中有一個進程試圖修改進程空間,這時父子進程就各自擁有了各自的進程空間,簡單地理解,從這一時刻器,父子進程就是兩個獨立的進程,誰都不會影響誰(實際上還是有一定影響的,在這里可以忽略),父子進程之間的關聯僅剩下它們共享的代碼段了。
對於system函數,我們可以先看一下它的源代碼:
int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL){ return (1); } if((pid = fork())<0){ status = -1; } else if(pid == 0){ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); -exit(127); //子進程正常執行則不會執行此語句 } else{ while(waitpid(pid, &status, 0) < 0){ if(errno != EINTER){ status = -1; break; } } } return status; }
我們看到system()函數實際上就是先執行了fork函數,然后新產生的子進程立刻執行了exec函數,我們前面說個fork函數換湯不換葯,exec函數換葯不換湯,那么system函數就是既換湯也換了葯,也就是system函數會產生新進程,這就意味着新進程的PID、PPID等與原進程不同。system也會產生新的進程空間,而且新的進程空間是為新的程序准備的,所以和原進程的進程空間沒有任何關系(不像fork新進程空間是對原進程空間的一個復制)。還要注意的是,system函數代碼中else部分執行了wait函數,這就意味着,原進程會等待子進程執行完畢(阻塞)
最后還要注意的一個問題是關於文件描述符的。
exec函數執行后,原來打開的文件描述符依然存在。
fork函數執行后,原來打開的文件描述符會復制一份到新的進程中,之后兩個進程之間的文件描述符就相對獨立了。
system函數先執行fork函數,這之后兩個進程的文件描述符就相對獨立了。之后exec函數並不影響文件描述符。