Linux下程式相關:fork(),wait(),exec()

山城過雨發表於2017-04-13

1採用命令列操作時,所建立程式的pid編號、程式執行、撤銷過程;

為實現此部分要求,我們編寫一小段程式。它的設計想法是,接收使用者的輸入,直到得到我們需要的輸入,才退出。當我們完成程式程式碼編寫,併成功編譯,執行這段可執行程式時,就建立了一個程式。程式建立後,可以通過ps命令檢視到該程式的資訊。該程式在接收到需要的輸入後正常退出,當然,也可以通過終端強制結束,這也就是程式的撤銷過程。

 

 

按照上述的思路,設計的程式程式碼如下:

 

1code1 程式程式碼

編譯執行該程式

 

2code1 程式的執行

通過在命令列啟動該程式,我們建立了一個程式,此時,通過ps命令來檢視該程式的資訊。

 

3code1 程式資訊

這裡,我們用到ps命令來進行程式的檢視,這個命令和top命令的區別在於:ps命令像為系統程式資訊拍了張快照,而top命令則像是現場直播,也就是說ps得到的是靜態的結果,而top得到的是實時的,動態的結果。

ps命令有許多的引數,常用的引數如下:

 

引數

含義

a

顯示當前終端機下所有程式

u

以使用者為主的格式來顯示程式狀況

x

顯示所有的程式,不以終端機為區分

e

顯示程式的環境變數

f

ascii字元顯示樹狀結構,顯示程式間的相互關係

l

顯示詳細資訊

c

顯示程式真正的指令名稱,而不包含路徑,引數等標示

表格1ps命令引數

 

常用的組合為ps aux ps ef,這裡我們使用了ps aux來檢視剛才建立的程式的資訊。使用管道和grep命令結合,以方便檢視。從圖3可以看到,我們建立的程式的pid編號為16588

 

 

4code1的正常執行和退出

從圖4可以看到,我們連續的輸入一串字元,在輸入回車後,程式正常退出,此時再檢視程式資訊


 

5code1正常退出

從圖5可以看到,程式正常退出後,再次檢視此時系統的程式資訊,此時已經沒有名為code1的程式。

 

再次執行該code1程式,這次我們使用終端強制終止的方法來結束建立的程式。

 


6code1 終結

 

 

7code1 強制退出

和之前介紹的類似,我們使用ps命令來檢視code1程式的資訊。通過kill命令來強制終止該程式。

 

Kill命令可以用來強制終結一個程式,它包含很多可以使用的訊號,但我們一般只會用到15 和 9,通過kill -l可以檢視所有的訊號。訊號15SIGTERM,這個訊號用來請求停止執行一個程式,但並不是強制停止,程式可以有時間來完成資源釋放等工作後,再停止。和訊號15不同的,訊號9SIGKILL,這個訊號強制程式立刻停止。Kill命令的使用格式為kill [訊號/選項] PID,預設的訊號是15,如果無效時,可以使用訊號9

 

從圖6可以看出,我們使用了kill命令(訊號15)來終結code1,程式提示Terminated,而我們使用帶訊號9kill命令時,程式提示為,killed

 

2採用系統函式呼叫時,程式的pid號、子程式建立前後的變化;

根據實驗指導的提示,編寫此部分的程式。其中關鍵之處為fork()函式的使用。fork()函式通過系統呼叫來建立一個和原程式幾乎一樣的程式。由fork()函式建立的新程式稱為子程式。子程式是父程式的克隆(副本),它可以獲得父程式的資料空間、棧、堆等資源的副本。fork()函式呼叫一次,能夠返回兩次,返回值有以下的三種情況

在父程式中返回建立的子程式的PID

在子程式中返回0

錯誤情況下,返回負值

 

編譯執行編寫的程式碼,通過檢視執行情況來加深對上述fork()函式相關內容的理解。

 

8code2.c 

 

9code2執行情況

從圖9可以看出,在執行code2後,輸出了兩條結果,根據我們之前的學習,這裡顯示了一條父程式的輸出和子程式的輸出。分析這個簡單的程式,不難知道,在我們呼叫fork()函式以前,只有一個程式(父程式)在執行這段程式碼,而在呼叫結束後,這時便由父程式建立了一個新的子程式。子程式和父程式共享程式碼段,在子程式中,fork()返回的值為0,故輸出“this is child process!”。在父程式中,fork()返回的值為子程式的PID,故輸出為“..child process id = 9617”

 

在這裡,最開始比較難於理解的便是這個程式的執行結果。因為else if{}else這是兩句互斥的選擇語句,怎麼也不可能在一個程式執行過程中同時被執行。雖然我們根據實驗指導書知道,該函式呼叫一次,返回兩次,但怎麼返回的,為什麼會輸出兩句,其中的具體細節,請我們還是很模糊。在學習了書中的相關理論後,我們在code2的基礎上增加兩句話。

 

 

10code2 new

即是如圖10所示中的第和第8行處的句子。可以推斷,這個執行code2的結果應該是:

 

before fork

after fork

this is the parent process,child process id=***

after fork

this is child process!

 

推出這樣的結果的根據是:在fork()函式之前,是父程式在執行,會輸出一句“before fork”,而一旦執行到fork(),系統會建立和父程式幾乎一樣的子程式,這個時候,父程式和子程式誰先執行就不得而知了,要看系統的排程策略。特別注意的是,fork()建立的子程式是針對父程式執行到fork()時的當前的狀態建立的,也就是說,在這個函式以前的程式碼,是不關子程式的事的。就像兒子沒法時光倒流去幹涉父親十幾歲的事,因為那個時候兒子都還沒出生。而一旦子程式產生,父子倆就分道揚鑣(如圖12所示),各幹各的,這也就是fork()函式為什麼叫fork的原因了。所以,至此,我們最開始的疑惑也就明白了,這不是程式的一次執行,而是對應著兩次執行過程——父子程式,在父程式中選擇分支選擇了else{},而在子程式中選擇分支選擇了else if{}

 


 

12fork示意

我們執行一下修改後的code2

 

13code2 new執行結果

從執行結果來看,我們的推斷基本正確。

 

除了這種方式外,我們還可以通過GDB來程式多程式的除錯。為了使用gdb除錯,我們需要在編譯時候加入除錯選項-g,然後根據實驗指導書的提示,設定多程式除錯模式,並進行相應的除錯工作。

 

通過ps命令來檢視程式的情況。

 

14code2程式

可以看到PID9617的子程式和它的父程式。

 

3父程式與子程式併發執行(父子程式完成相同計算量的任務,單個任務計算時間大於3秒),分兩種情況:程式數量少於空閒cpu數目、程式數量大於空閒cpu數目兩種情況,比較一個程式完成時間,給出時間差別的解釋。

 

首先分析第三步我們需要做的事情,首先需要父子程式併發執行,我們知道fork()產生的子程式和父程式就是併發執行的;而在需要完成的計算任務設計上,參考上學期的演算法設計課程,選取一個較為耗時的演算法即可,比如某種排序演算法;我們知道在單處理器上,多程式併發就是實際上就是時間片的輪換利用,而這個輪換也是需要需要時間的,也就是我們的處理機資源只有一個,不能做到真正的併發,而在多處理器機器上,多工的多程式併發優勢可以得到很好的體現,因為可以將多個程式分配到不同的處理器上,從而可以提交執行效率,這應該也是為什麼實驗指導中需要我們考慮程式數量和空閒處理器的緣故。在對要求有了一定的瞭解後,下面開始此部分的實驗。

 

15code3single

15展示了一個簡單的排序程式,為了達到單個任務計算時間超過3s的要求,我們使用了較大的資料規模,並用了最簡單也是效率最低的簡單氣泡排序。

 

16code3single執行結果

16展示了我們編寫的code3single的執行時間結果,可以看到,程式完成1024*32個資料的排序,共用時約3.7s

 

根據實驗指導書的提示,我們將上面的程式碼修改為父程式建立一個子程式,然後父子程式完成相同計算任務的程式碼。

虛擬碼如下:

//code3.c

begin

pid = fork();

if pid == -1 then

  return error;

else if pid == 0 then

  sort();

else then

  sort(); 

end

 

因為程式較為簡單,就不展示完整的程式碼,code3.ccode3signal.c的區別僅僅在於,我們建立了一個子程式,並在父程式和子程式都進行了排序工作。

 

在執行code3前,檢視cpu的使用情況:

 

17CPU使用情況

我們通過htop工具來檢視cpu的使用情況,可以看到,實驗機器為4核處理器,且均未完全使用。

 

接著我們執行code3,也就是此時,空閒cpu數是多於我們的建立的程式數(父程式和子程式,兩個)的。

 

18code3執行結果

code3的執行結果顯示,不論是父程式還是子程式,執行和code3single同樣的計算任務,時間差別並不是很大,當同時在兩個終端下執行code3code3single,兩者得到的時間差更小。再來看看,如果我們的建立的程式數多餘空閒的cpu數時,程式執行的情況。

 

可以通過減少空閒cpu數和增加程式的方法來滿足實驗要求的條件,我們先選擇減少空閒的cpu數,即用其他的計算任務來佔據空閒的CPU資源。

 

19CPU使用情況2

從圖19可以看到,我們通過執行其他的計算任務,使得,空閒的cpu數為1,也就是圖中看到的,123號均達到了100%的使用率,這時候,我們再執行code3,看看結果如何。

 

 

20code3執行結果2

 

從圖20的結果來看,此時父程式和子程式的執行時間大概是圖18展示的code3cpu有較多空閒的情況下的執行時間的三倍。

 

我們再嘗試增加程式數,比如增加到五個(超過空閒cpu數,4個)。這裡為了增加結果的可靠性,我們併發執行五個子程式,它們完成相同的計算任務,而在父親程式中,我們利用waitpid方法來進行阻塞,父親程式在所有程式完成後,再進行子程式相同的計算任務。

//code3more.c

Begin

int pid1 = fork();

if pid1 == 0 then

sort();

exit();

int pid2 = fork();

if pid2 == 0 then

sort();

exit();

...

waitpid(pid1,NULL,0);

waitpid(pid2,NULL,0);

.....

sort();

End

code3more.c的虛擬碼如上,我們建立了五個子程式,它們會併發執行,多於空閒的cpu4,而父程式等待子程式完成後,再完成計算任務,當然,此時進行我們計算的程式數(只有父程式)少於空閒CPU數。值得注意的是,我們沒有讓子程式和父程式併發,這裡和題目要求略有差別


21code3more執行結果

如果我們讓父子程式併發,即註釋掉waitpid部分,執行結果如下:

22code3more執行結果2

可以看出,和圖21所示的結果相比較,最大的區別在於,父程式會出現先於子程式完成,子程式變成了孤兒程式,當然,這是我們不太希望看到的,所以,此處根據實際情況,只讓幾個子程式進行併發。

 

再來看圖21的結果,和圖18cpu空閒狀態下,父子程式併發執行的時間相比,幾個子程式耗費的時間均在9~10s大於圖18中的4s。分析可能的原因是,當cpu空閒數較多的時候,我們的這幾個計算程式不需要進行過多的程式排程,因而完成計算任務花費時間較少,和單個程式的時間幾乎相同,而當我們的計算程式多於空閒cpu數時,發生了較多的程式排程,而程式排程是需要較大的時間開銷的,所以,此時完成計算任務所需的時間就會多些。圖20的結果也說明了這一點,圖20的結果是在我們用其他計算任務佔用cpu,使得空閒cpu數為1的時候得到的,此時會發生的排程會更多,因而時間開銷也略會更大一些。

 

 

4父程式等待子程式完成(可以使用阻塞的wait()呼叫),觀察記錄父子程式的就緒和阻塞狀態變化過程(用/proc檢視程式的狀態);

首先使用搜尋引擎查閱wait()函式相關的知識。

 

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid,int * status,int options);

 

提到wait函式就不得不談到waipid函式,從系統的角度看,兩個函式的功能是一樣的(只是waitpid多了兩個供使用者選擇的引數),那就是分析當前程式的某個子程式是否已經退出,如果已經子程式退出,wait(或者waitpid)就會收集這個子程式的資訊,並且把它銷燬,然後返回,如果沒有這樣一個子程式,wait(或者waitpid)就會一直阻塞當前程式,直到出現一個這樣的子程式。

 

23code4.c程式碼片段

我們直接在code3.c的基礎上,稍微了修改一下,用作本部分的實驗程式碼,所以,僅僅給出主函式部分。在父程式中,呼叫wait()方法,阻塞父程式,此時父程式只有等待子程式完成後,才能就緒,執行。為了便於觀察,我們讓父程式在輸出子程式返回資訊後,繼續執行一段計算程式碼。也就是說,我們看到父程式列印出了子程式的返回資訊時,就知道子程式已近執行完畢,這時父程式應該不再是阻塞狀態了。在子程式的退出時,返回2,在父程式中利用去得到子程式退出時的返回值。這裡用到了兩個巨集,WIFEXITED(int status),當子程式正常退出("exit""_exit"),此巨集返回非0WEXITSTATUS(int status),獲得子程式exit()返回的結束程式碼。

 

 

24code4執行結果1

從執行結果可以看到,父程式只有等到子程式執行完成後(獲得了子程式退出時返回的結束程式碼),才能就緒,執行。

 

25code4 ps結果

我們可以在code4執行時候,使用ps命令簡單地檢視一下父子程式的狀態,可以看到pid號為29270的程式(也就是父程式),是處於S狀態的,而子程式正在執行。當然我們也可以通過/proc來檢視程式的詳細資訊。

 

執行code4,然後使用命令cat /proc/[pid]/status來檢視對應程式的狀態資訊。

這條命令會返回pid對應的程式(如果存在的話)的詳細資訊,這裡整理處幾個常用的資訊。

引數

含義

Name

應用程式或命令的名字 

State 

任務的狀態,執行/睡眠/僵死/ 

Tgid

執行緒組號 

Pid 

任務ID 

Ppid 

父程式ID 

VmRSS(KB)

應用程式正在使用的實體記憶體的大小

 

 

26code4執行2

我們利用ps命令來檢視code4的程式號,這裡也可以看到,在code4剛開始執行時,有兩個程式,程式號分別為3077230773,其中30772為我們的父程式,此時,它被wait()阻塞,所以是S狀態,子程式30773正處於執行狀態,等到子程式結束後,父程式結束阻塞狀態,就緒,執行,所以,我們可以看到,此時,30772狀態變為R。再來看看,由/proc得到的結果。

 

27code4 /proc狀態檢視結果

從圖25可以看出,30772(父程式)剛開始處於S狀態,而此時的子程式30773處於R狀態,圖中圈出,30773Ppid,也就是是父程式pid30772。而等到子程式執行完畢,再次檢視30772的狀態,可以看到,變為了R狀態。

 

5父子程式執行不同的可執行檔案(需要利用exec()呼叫),完成不同功能;

先查閱相關的資料。

Linux中並沒有一個名為“exec”的函式,而是六個以exec開頭的函式族,它們是:

標頭檔案

#include<unistd.h>

函式原型

int execl(const char *path, const char *arg, ...)

int execv(const char *path, char *const argv[])

int execle(const char *path, const char *arg, ..., char *const envp[])

int execve(const char *path, char *const argv[], char *const envp[])

int execlp(const char *file, const char *arg, ...)

int execvp(const char *file, char *const argv[])

返回值

成功:不返回

失敗:返回-1

 

表中前四個函式以完整的檔案路徑進行檔案查詢,後兩個以p結尾的函式,可以直接給出檔名,由系統從$PATH中指定的路徑進行查詢。這裡不同的函式字尾,代表著的含義是:

 

字尾

含義

l

接收以逗號分隔的引數列表,列表以NULL指標作為結束標誌

v

接收到一個以NULL結尾的字串陣列的指標

p

是一個以NULL結尾的字串陣列指標,函式可以通過$PATH變數查詢檔案

e

函式傳遞指定引數envp,允許改變子程式的環境,無字尾e時,子程式使用當前程式的環境

 

值得注意的是:這六個函式中真正的系統呼叫只有execve(),其他的都是庫函式,它們最終都會呼叫到execve();exec函式常常會因為找不到檔案,或者沒有對應檔案的執行許可權等原因而執行失敗,所以,在使用是最好加上錯誤判斷語句。

 

fork()函式產生的子程式和父程式幾乎一樣,也就是父子程式完成相同的工作,而exec()函式則可以讓子程式裝入或執行其他的程式,也就是可以做和父程式不一樣的事。根據對查閱的資料理解,結合前面部分的實驗,得到本部分的實驗程式碼:

 

28code5.c程式碼片段

我們在子程式中呼叫了execvp()函式,根據前面的資料,這個函式的第一個引數就是我們呼叫的shell命令或者是要執行的檔案;第二個參數列示這個函式希望接收一個
NULL結尾的字串陣列的指標,我們這裡定義了char *arg1[] = {"./code5child", , NULL},char *arg2[] = {"./code5parent", , NULL};為了便於觀察,我們使用wait()函式,使得子程式執行完畢,父程式再繼續執行。

code5childcode5parent為兩個我們的測試檔案,它們的執行結果為:

 

29code5childcode5parent執行結果

編譯執行code5

 

30code5:執行結果

從圖27可以看到,我們讓子程式執行了“./code5child”,列印了一句“this is child here!”;而父程式則沒有做這項工作,它執行了“./code6parent”,列印了一句“this is parent here!”。可以看出,我們通過exec()函式呼叫來實現父子程式執行不同可執行檔案的目的。

 

當然,我們也可以讓子程式執行一條shell命令,比如下面的圖28所示的結果:

 

31:子程式執行shell命令

6生成3層或以上的父子程式樹,用/proc檔案檢視它們的父子關係

當父程式呼叫fork()函式的時候,便建立了一個子程式,而父子程式是相對的,也就是子程式中再呼叫fork()時,子程式就建立了它自己的子程式,它是該子程式的父程式。

 

本部分的實驗程式碼如下:

 

32code6.c程式碼片段

從圖32的程式碼片段可以看出,我們在一個父程式下建立了三個子程式,pid1pid2pid3,然後,在pid1下,又建立了一個子程式subpid,它是pid1的子程式,父程式的孫程式

我們先使用pstree命令來檢視樹狀的程式關係。

 

33pstree得到的程式樹

從圖33的結果可知,我們建立了五個程式,212443個子程式,它們是21245,21246,和21248,而2124721245的子程式。當然,我們也可以使用/proc來檢視程式之間的父子關係,如圖27中展示的那樣。


34/proc檢視code6

從圖34可以看出,22168為父程式,它是22169的父程式,22169又是22170的父程式(圖中PPid表示父程式pid號的意思)。

實驗原始碼:連結: https://pan.baidu.com/s/1kV7Jfq3 密碼: itiz


相關文章