1. 進程退出場景
進程退出一般有三種場景:
。代碼運行完畢,結果正確
。代碼運行完畢,結果錯誤【比如,我們要對某個文件進行寫入,但寫入的文件路徑出錯,代碼運行完畢,可是結果出錯】
。代碼異常終止【這種情況的原因有很多種,我們在寫C/C++代碼時肯定遇到過】
一般進程的退出結果是交給父進程的,因為父進程創建子進程是為了讓子進程幫父進程完成某種任務,所以父進程肯定需要拿到子進程的退出結果。
那父進程是如何拿到子進程的退出結果呢?我們下面慢慢說。
2. 進程常見退出方法
2.1 從main函數返回
我們以往在寫C/C++程序時,通常會在main函數的結尾返回0,但至于這個返回值的作用是什么,我們不知道,并且我們也不關心!但是,我們現在有必要知道了:
main函數的返回值通常表明你程序的執行情況!
。return 0;表明代碼運行完畢,結果正確
。return 非0;表面代碼運行完畢,結果錯誤【不同的值表明不同的出錯原因】
下面,我們來寫一段代碼來見一見main函數的不同返回值:
以上代碼,我們打開一個文件進行讀的操作,但是在該目錄下,我并沒有這個文件,所以肯定是打開失敗的。所以函數會返回1。?
但是,我們怎么知道這個進程的退出結果呢?
這里補充一個概念:退出碼【退出碼(退出狀態)可以告訴我們最后?次執行的命令的狀態。在命令結束以后,我們可以知道命令 是成功完成的還是以錯誤結束的。其基本思想是,程序返回退出代碼0 時表示執行成功,沒有問題。 代碼1 或0 以外的任何代碼都被視為不成功】。
每個進程退出后進入僵尸狀態,其退出碼都會寫入自己的task_struct中【exit_code】。
我們只要在命令行上敲下以下指令就可以查看到最近一個進程退出時的退出碼:
其實,在C標準中,規定了每個退出碼都有自己對應的錯誤信息,strerror就可以把退出碼轉化成對應的錯誤信息!
下面我們就來看一下C標準規定了多少錯誤信息:
可以看到從0開始一共有134個退出碼!
最后,如果進程異常終止,退出碼就不重要了,就好比你考試作弊被發現了,你的結果也就不重要了。
2.2 exit函數
其實除了從main中用return返回結束進程,我們還可以用exit函數結束進程。
并且在程序的任何地方調用該函數,進程都會直接退出并返回退出碼給父進程!
?只打印“fun begin”驗證了這一點!
2.3 _exit函數
如果exit退出的時候,進程退出的時候,會進行緩沖區刷新。
如果_exit退出的時候,進程退出的時候,不會進行緩沖區刷新。
以上這一點本身并不重要,了解即可。
我們知道庫函數和系統調用是上下級關系,即exit的底層還是_exit,但為什么_exit卻不會刷新緩沖區呢?
因為緩沖區是庫級別的【C語言提供的】,也就是庫緩沖區,這里只點出來,更多細節,后面再說。
3. 進程等待
3.1 為什么要有進程等待
? 之前講過,子進程退出,父進程如果不管不顧,就可能造成‘僵尸進程’的問題,進而造成內存泄漏。
? 另外,進程?旦變成僵尸狀態,那就刀槍不入,“殺?不眨眼”的kill-9也無能為力,因為誰也沒有辦法殺死?個已經死去的進程。
? 最后,父進程派給子進程的任務完成的如何,我們需要知道。如,子進程運行完成,結果對還是 不對,或者是否正常退出。
? 父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息
3.2 進程等待是什么
要搞清楚這個問題,我們先來見一見進程等待先簡單使用一下!
a. wait方法
wait其中的參數wstatus其實是輸出型參數,這點我會在waitpid處再詳細說明。?
這里我們先使用wait方法解決僵尸進程的問題:
以下代碼我先創建了一個子進程,在子進程運行3秒后退出,而父進程先休眠5秒后,再使用wait等待子進程,最后讓父進程運行10秒后退出。這樣做的原因是方便我們檢測時,先看到子進程的僵尸狀態,父進程等待成功后,我們可以看到子進程從僵尸狀態被回收的過程。
1#include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <wait.h>5 #include <stdlib.h>6 7 int main()8 {9 pid_t id=fork();//創建子進程10 if(id==0)//子進程11 {12 int cnt=3;13 while(cnt--)14 {15 printf("子進程pid->%d\n",getpid());16 sleep(1);17 }18 exit(0);19 }20 //父進程等待子進程21 sleep(5);//父進程先等待5秒,可以看到子進程的僵尸狀態22 int exit_code=0; 23 pid_t rid=wait(&exit_code);24 if(rid>0) printf("等待子進程成功!rid->%d\n",rid);25 sleep(10);26 return 0;27 }
運行結果:
下面是檢測寫的簡單shell腳本:?
while :; do ps axj | head -1 && ps axj | grep code; sleep 1;done
檢測結果:
結果也確實和我們預期的一樣!
這里還有兩個細節 ,如果父進程在等待子進程的中,子進程沒有退出,那么父進程就在wait處等待【可以理解為scanf】!父進程wait時會等待它任意一個子進程退出并獲得其pid。
b. waitpid方法
waitpid有3個參數,其中第一個參數有以下四種填寫方法,目前我們先不管<-1和=0,-1表明等待任意子進程退出,>0則表明等待指定子進程退出。?
?第二個參數為輸出型參數,我們之前便說過,父進程創建子進程是為了讓子進程幫父進程完成某些任務。所以父進程應該有能力知道【不管它想不想知道】子進程的退出結果【即退出信息】,而第二個指針參數就是為了做到這一點而存在的,而wstatus獲得的正好就是子進程的退出碼!
第三個參數可以控制阻塞【我們先暫時不管】
下面我們來看看waitpid到底能不能獲得子進程的退出碼:
按照我們的預期,exit_code應該是1!
但是結果似乎和我們預期的不太一樣!
當實際情況和預期不一樣時,那一定是我們的認知出了問題!
這里就直說了,整形的exit_code接收退出碼時,只用了32個比特位中的8~15這8個比特位來記錄退出碼【我們可以通過位圖來理解】!而16~32這16個比特位沒有被使用,至于1~8這8個比特位在子進程正常退出時都默認是0,而只有在子進程異常退出時,這八個比特位才會被使用,這和信號有關,細節我們以后再說!
既然如此,那我們拿到記錄退出碼的那八個比特位就可以看到正確的退出碼了!
修改這一句代碼即可!
if(rid>0) printf("等待子進程成功!rid->%d,exit_code->%d\n",rid,(exit_code>>8)&0xFF);
還有一個問題,我們為何不定義一個全局變量來記錄子進程的退出碼呢,在子進程退出時,修改這個變量!原因也很簡單,你子進程修改它,發生寫時拷貝,父進程和子進程看到的全局變量根本就不是同一個!
最后,如果進程異常退出,退出碼就沒有意義了,但是,我們還是可以通過waitpid的輸出型參數拿到退出信號。
稍微修改一下代碼,做一個實驗:
手動殺掉進程!父進程得到相應的退出信號!
?我們可以通過指令kill -l來查找所有的信號:
這里只是簡單的見了見信號,具體細節還沒有說!
?3.3 進程等待怎么辦到
現在,我們理解了什么是進程等待,為什么要有進程等待,但是我們還是不知道,父進程通過進程等待是如何拿到子進程的退出碼和退出信號的!
要明白這個問題,我們首先需要知道這些信息存放到哪里!我們都知道一個進程退出后,它的進程虛擬空間 | 頁表 | 代碼和數據 都會被系統回收和釋放!但是,他的PCB【task_struct】卻會被保存起來,進入僵尸狀態!所以,子進程的退出碼和退出信號勢必在它的PCB中!所以,進程退出時,它的退出信息會被記錄進它的PCB中的某些變量中!
事實也是如此:在源碼中記錄信息的變量如下所示
所以,父進程通過系統調用的方式讓系統幫它去子進程的PCB中拿到子進程的退出信息返回給父進程。至此,我們也就明白了,進程等待的原理了。而且,我們現在也更加明白了,為什么進程要有僵尸狀態【1. 為了方便父進程回收子進程 2. 為了父進程方便拿到子進程的退出信息】
3.4 阻塞與非阻塞等待
上面我們提到過waitpid的第三個參數使用來控制阻塞和非阻塞調用的,在默認情況下【不填參數】,waitpid進行的是阻塞調用,什么是阻塞調用呢?
阻塞調用:父進程在waitpid處等待子進程退出,如果子進程一直不退出,那父進程則會一直等待,并且只做這一件事情!
如果,我們將參數填寫為WNOHANG,則為非阻塞調用,非阻塞調用:父進程每隔一段時間去看一看子進程是否退出,如果退出,則返回值大于0,如果調用結束,子進程沒有退出,則返回0,如果返回值小于0,則調用失敗。在子進程未退出之前,父進程可以利用其他時間做一些其他的事情,所以非阻塞調用一般效率更高【并不是指子進程退出的效率高】。
如何做到非阻塞等待,其實也很簡單,我僅們需要做一次非阻塞輪詢,說白了,就是循環!
下面一個例子就很好的體現了非阻塞調用下父進程利用等待時間完成其他的任務。?
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
typedef void (*handler_t)(); // 函數指針類型
std::vector<handler_t> handlers; // 函數指針數組
void fun_one() {printf("這是?個臨時任務1\n");
}
void fun_two() {printf("這是?個臨時任務2\n");
}
void Load() {handlers.push_back(fun_one);handlers.push_back(fun_two);
}
void handler() {if (handlers.empty())Load();for (auto iter : handlers)iter();
}
int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { // childprintf("child is run, pid is : %d\n", getpid());sleep(5);exit(1);}else {int status = 0;pid_t ret = 0;do {ret = waitpid(-1, &status, WNOHANG); // ?阻塞式等待 if (ret == 0) {printf("child is running\n");}handler();} while (ret == 0);if (WIFEXITED(status) && ret == pid) {printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else {printf("wait child failed, return.\n");return 1;}}return 0;
}