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

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


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