目錄
一、進程創建
1.寫時拷貝
2.創建多個進程
二、進程終止
1.main函數的返回值
2.bash中的$??
3.自定義退出碼
4.C語言的錯誤碼
5.錯誤碼與退出碼的區別
6.代碼異常終止
7.exit函數
8.總結
一、進程創建
在之前,我們學過linux中的非常重要的函數——fork。他可以從已存在進程中創建一個新進程,新進程為子進程,而原進程為父進程。
1.寫時拷貝
我們知道,fork之后,父子代碼共享,經常會出現同一個變量,父子通過操作的不同,這個變量的值也不同,這個時候就會發生寫時拷貝。寫時拷貝是如何進行的呢?
通過這張圖可以看到,fork之后數據段變成了只讀, 子進程需要對數據進行寫入,就得需要寫時拷貝,寫時拷貝需要重新申請空間,進行拷貝,再修改頁表,這都是操作系統在幫我們處理的,那么操作系統怎么知道你這一份數據需要進行寫時拷貝呢?
父進程創建子進程的時候首先將自己的讀寫權限修改成只讀,然后再創建子進程,這些操作用戶并不知道,可能對某些數據進行寫入,這樣在頁表處就會進行權限判斷,發現用戶沒有權限,操作系統此時就會介入,操作系統會判斷用戶的操作
如果該區域本該是可讀可寫的,是操作系統修改為只讀的,因此操作系統會認為用戶的操作不算錯誤,就會觸發重新申請內存再拷貝內容的策略機制,這就是寫時拷貝。
如果出錯,就直接報錯,不做額外處理。
寫時拷貝完成后,再將對應的內容在頁表中修改為可讀可寫(沒有進行寫實拷貝的內容依然是只讀的)。這樣用戶就可以正常訪問了。
這是一種惰性分離,每次發生寫時拷貝都要開辟空間,將寫時拷貝的時間越往后延遲,操作系統就有更多的資源。
這里還有一個小問題:你要寫入的時候寫就完事了,為何還要拷貝一份呢?
因為覆蓋和修改是不一樣的,很多情況,我們只是想要修改內容的某一部分,這樣先拷貝再修改會更合適一點。
2.創建多個進程
我們知道fork的常規用法如下兩種
- 一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子 進程來處理請求。
- 一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。
如果要創建多個進程來幫我們處理,應該怎么做呢?? 直接上代碼
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>#define N 10typedef void (*callback)();void Work()
{int cnt = 10;while(cnt){printf("我是一個子進程, pid: %d, ppid :%d, cnt:%d\n",getpid(),getppid(),cnt--);sleep(1);}
}void CreateProcess(int n,callback cb)
{int i = 0;for(;i<n;i++){sleep(1);pid_t id = fork();if(id == 0){//child printf("子進程創建成功: %d\n",i);cb();exit(0);}}
}int main()
{CreateProcess(N,Work);sleep(100);return 0;
}
這代碼對于學過fork的我們來講,并不算難,多了一個函數指針而已,下面是運行代碼。
二、進程終止
進程退出的場景如下三種
- 代碼運行完畢,結果正確
- 代碼運行完畢,結果不正確
- 代碼異常終止
1.main函數的返回值
我們寫C語言程序時,main函數一般都會return 0。只要執行到了return語句,證明我們的代碼肯定是運行完畢了的,只是結果還不知道是否正確。
在多進程環境中,我們創建子進程的目的是完成父進程不方便辦的事,那我們怎么知道子進程辦得怎么樣,雖然我們可以打印出來看看結果,但在有一些情況下不方便或者不能打印出來看看,此時就可以通過return的值來查看的,main函數的返回值,就叫做進程的退出碼,0通常表示成功,非0表示失敗。父進程可以通過獲取子進程退出碼(即main函數的返回值)來得知子進程做得咋樣。
成功的還好,知道你吧事情辦得很好,如果返回非0,代表這個事沒辦好,我們得知道是因為什么原因失敗的,我們可以用不同的數字表示不同的原因。但純數字能表示出錯的原因,但是不便于人閱讀,因此有一個函數交 strerror 函數。
如下可以打印出strerror各個數字代表的出錯原因
有很多很多原因?
2.bash中的$??
在bash命令中輸入echo $? 可以打印出最近一個子進程執行完畢時的退出碼,有點類似于之前我們學習的環境變量,變量名為?,加了$可以打印出變量里的內容。
如下代碼中return 10,執行該進程,bash最后獲取到的子進程退出碼就為10
但是我們繼續執行echo $? 后面退出碼就會變成0,因為echo也是bash的一個子進程,執行echo語句后,echo語句就是最后一個子進程了,echo又是正常退出的,因此再輸入echo $? 得到的值為0。
main函數的退出碼是可以被父進程獲取的,用來判斷子進程的運行結果?
3.自定義退出碼
退出碼可以使用C語言內置的,也可以自定義,自己對退出碼做解釋,因為退出碼退出多少(也就是return 返回多少是你自己設置的)?
如下就是自定義的退出碼,如果你的代碼根據用戶的操作出現了錯誤,可以返回響應的值,來知道發生了什么錯誤。
4.C語言的錯誤碼
在學習C語言的時候,我們接觸過一個名叫 errno 的全局變量,他會在程序在運行過程中調用某些庫函數或者系統接口出錯的時候,被自動設置。也是記錄最后一次出錯的信息。
如下代碼,只讀的方式打開一個不存在文件,我們看一下erron的變化與出錯信息
發現錯誤碼為2,錯誤信息為沒有該文件
5.錯誤碼與退出碼的區別
- 錯誤碼通常是衡量調用庫函數或者系統調用接口的調用情況。(系統調用也能更改錯誤碼是因為Linux是用C語言寫的,提供了C式接口)
- 退出碼通常是一個進程退出的時候,他的退出結果。
他們兩個共同的地方在于當失敗的時候,用來衡量函數、進程出錯時的詳細原因。
如下,讓錯誤碼與退出碼保持了一致
6.代碼異常終止
前面五點主要學習的是進程正常退出的問題,可能會有出錯碼和退出碼,如果一個進程異常終止,那么他的退出碼也就沒有了意義。
比如代碼中存在 /0 錯誤,又比如段錯誤,棧溢出等等,程序就會崩潰,進程就異常了,就不會繼續運行了,本質是操作系統將該進程殺掉了,操作系統會用信號的方式將進程殺掉。
輸入 kill -l 可以查看 kill命令的信號?
這里我們一直運行一個進程,然后輸入kill -8 + 進程pid,就可以通過浮點數錯誤的方式終止該進程。輸入其他方式殺死,也會有相應的錯誤報告。?
?因此,查看進程是否出現異常,我們只需看有沒有收到信號即可。
7.exit函數
C語言退出函數 exit() ,括號內部可以添加數字,這也是退出碼的一種。?
exit與return的區別在于:
在非main函數中return 并不會終止進程,main函數會終止進程。
在任意函數中exit都會終止進程。
8.總結
查看進程運行完畢,結果是否正確,只需要看退出碼即可
查看進程異常終止,只需要查看收到的信號是什么即可。