目錄
三、進程等待
1)進程等待的必要性
2)獲取子進程的status
3)進程的等待方法
wait方法
waitpid方法?
多進程創建以及等待的代碼模型??
非阻塞的輪訓檢測
四、進程程序替換
1)替換原理
2)替換函數
3)函數解釋?
4)命名理解?
三、進程等待
1)進程等待的必要性
- 之前提過子進程退出,父進程如果不讀取子進程的退出信息,就可能造成“僵尸進程”的問題,從而造成內存泄漏的問題。
- 再者,一旦子進程進入了僵尸狀態,那就連kill -9都殺不亖他,因為沒有誰能夠殺亖一個死去的進程。
- 最后,父進程創建子進程是要獲取子進程的完成任務的情況的。
- 父進程需要通過等待的方式來回收子進程的資源,獲取子進程的退出信息。
2)獲取子進程的status
下面進程等待使用的兩個方法wait方法和waitpid方法都有一個status參數,這是一個輸出型參數(輸出型參數是函數中用于返回結果或修改調用者變量的參數,通常通過引用或指針實現。如void func(int *output)。),由操作系統進行填寫。
如果向status中傳遞的是NULL,那就表示用戶不關心子進程的退出狀態。否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。
status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位):
我們從圖中可見,status的低16比特位中,高8位表示進程的退出狀態,即退出碼。當進程被信息殺亖時,則低7位表示終止信息,第8位時core dump標志。
我們可以通一系列的位操作來得出進程的退出碼和退出信號。
exitcCode = (status >> 8) & 0xFF; //退出碼exitSignal = status & 0x7F;
對于這兩個操作,系統提供了兩宏來獲取退出碼以及退出信號。分別是:
- WIFEXITED(status):用于查看是否是正常退出,本質是檢查是否收到信號。
- WEXITSTATUS(status):用于獲取進程的退出碼。
exitNormal = WIFEXITED(status); //是否正常退出exitCode = WEXITSTATUS(status); //獲取退出碼
敲黑板:
當一個進程是非正常退出的時候,那么該進程的退出碼將毫無意義。
3)進程的等待方法
wait方法
函數類型:pid_t wait(int* status);
返回值:成功返回被等待進程pid,失敗返回-1。
參數:輸出型參數,獲取子進程退出狀態,不關心則可以設置成為NULL?
作用:等待任意子進程?
創建子進程后,父進程使用wait方法等待子進程,直到子進程的退出信息被讀取,我們可以寫個代碼驗證一下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h> int main()
{ pid_t id = fork(); if(id == 0){ //子進程 int count = 10; while(count--) { printf("我是子進程,PID:%d, PPID:%d\n", getpid(), getppid()); sleep(1); } exit(0); } //父進程 int status = 0; pid_t ret = wait(&status); if(ret > 0){ printf("等待成功...\n"); if(WIFEXITED(status)){ printf("退出碼:%d\n", WEXITSTATUS(status)); } } sleep(3); return 0;
}
然后我們可以在開一個會話用來監控進程的狀態:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;echo "============================================================";sleep 1;done
?在下面這圖中我們可以看到,當子進程退出,父進程讀取到了子進程的退出信息時,子進程就不會變成僵尸狀態了。
waitpid方法?
函數原型:pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 當正常返回的時候waitpid返回收集到的子進程的進程ID;
- 如果設置了選項WNOHANG(option),而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
- 如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;?
參數:
- pid:當pid=-1,等待任意一個子進程,與wait等效。當pid>0.等待其進程ID與pid相等的子進程。
- status:輸出型參數,用來獲取子進程的退出狀態,不關心可以設置成NULL。
- options:默認為0,表示阻塞等待;當設置為WNOHANG時,若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子進程的ID。
返回值:等待任意子進程(可以指定)退出?
我們可以寫個代碼來驗證一下,創建子進程后,父進程可以使用waitpid函數一直等待子進程,直到子進程退出后讀取子進程的退出信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h> int main()
{ pid_t id = fork(); if(id == 0){ //子進程 int count = 10; while(count--) { printf("我是子進程,PID:%d, PPID:%d\n", getpid(), getppid()); sleep(1); } exit(0); } //父進程 int status = 0; //pid_t ret = wait(&status); pid_t ret = waitpid(id, &status, 0); if(ret >= 0){ printf("等待成功...\n"); if(WIFEXITED(status)){ printf("退出碼:%d\n", WEXITSTATUS(status)); }else{ printf("被信號殺?:%d\n",status & 0x7F); } } sleep(3); return 0;
}
在父進程運行過程中,我們可以使用kill -9命令來將子進程殺亖,這個時候父進程也能成功等待子進程。
敲黑板:
被信號殺亖的進程的退出碼是沒有意義的。
多進程創建以及等待的代碼模型??
上面演示的都是父進程的創建以及等待一個子進程,那么接下來我們可以同時創建多個子進程,然后讓父進程進程依次等待子進程退出。
下面我們可以同時創建10個子進程,將子進程的pid放到一個id數組中,并將這10個子進程的退出時的退出碼設置為該子進程pid對應數組中的下標,之后父進程使用waitpid等待這10個子進程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> int main()
{ pid_t ids[10]; for(int i = 0; i < 10; i++){ pid_t id = fork(); if(id == 0){ printf("子進程創建成功:PID:%d\n", getpid()); sleep(3); exit(i); } ids[i] = id; } for(int i = 0; i < 10; i++){ int status = 0; pid_t ret = waitpid(ids[i], &status, 0); if(ret >= 0){ printf("等待子進程成功:PID:%d\n", ids[i]); if(WIFEXITED(status)){ printf("退出碼:%d\n", WEXITSTATUS(status)); } else{ printf("被信號殺亖:%d\n", status & 0x7F); } } } return 0;
}
運行完代碼,我發現父進程同時創建了了多個子進程,當子進程退出后,父進程再以此讀取這些子進程的退出信息。?
殺亖進程,父進程依然可以獲取子進程的退出信息。
非阻塞的輪訓檢測
上面的方案,其實是有缺點的,那就是在父進程等待子進程的時候,父進程什么也干不了,這樣的等待就是阻塞等待。
而實際上,我們是可以讓我們的父進程在等待子進程退出的過程中做一些自己的事情的,這樣的等待就是非阻塞等待了。
其實想要實現非阻塞等待也很簡單,我們在上面說waitpid時就提過了,在想這個函數第三個參數傳入WNOHANG時就可以使得在等待子進程時,如果waitpid函數直接返回0,就不予等待,而等待的子進程若是正常結束,就返回該子進程的pid。
比如,父進程可以選擇地調用wait函數,若是等待的子進程還沒有退出,那么父進程就可以先去做一些其他的事情了,過一段時間在調用waitpid函數讀取子進程的退出信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> int main()
{ pid_t id = fork(); if(id == 0){ int count = 3; while(count--){ printf("子進程在運行:PID:%d, PPID:%d\n", getpid(), getppid()); sleep(3); } exit(0); } while(1){ int status = 0; pid_t ret = waitpid(id, &status, WNOHANG); if(ret > 0){ printf("等待子進程成功。\n"); printf("退出碼是:%d\n", WEXITSTATUS(status)); break; } else if(ret == 0){ printf("父進程做了其他事情。\n"); sleep(1); } else{ printf("等待錯誤。\n"); break; } } return 0;
}
代碼的運行結果就是,父進程每隔一段時間就去看看子進程是否退出,如果沒就去做自己的事情,知道子進程退出后讀取子進程退出的信息。
?
四、進程程序替換
1)替換原理
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。
?回答兩個關于進程程序替換的文題:
問題一:
當進行進程替換時,有沒有創建新的進程呢?
進程程序替換之后,該進程的PCB、進程的地址空間以及頁表等數據結構都沒有發生改變,而只是對原來在物理內存上的代碼和數據進行了替換,所有根本沒有創建新的進程,而且進程的程序替換前后其pid也是沒有改變的。
問題二:
子進程進行進程的程序替換時,是否會影響父進程的代碼和數據呢?
不會影響,一開始子進程被父進程創建時代碼和數據是和父進程共享的,但是一旦子進程要進行程序替換操作,就意味著子進程需要對代碼和數據進行寫入操作了,這時就需要對父進程的代碼和數據進行拷貝了(寫時拷貝),從這里開始子進程和父進程的代碼和數據就分離了,所有子進程進行進程的程序替換時不會影響父進程的代碼和數據。
2)替換函數
這里的替換函數都是以exec開頭的,所以稱之為exec函數,總共有六種:
1??int execl(const char *path,const char *arg,...);?
相關參數的說明:第一參數是要執行程序的路徑,第二個參數是可變參數列表,表示具體如何執行這個程序,同時以NULL結尾。
寫一個執行ls程序:
execl("/usr/bin/ls", "ls", "-l", NULL);
2??int execlp(const char *file, const char *arg,...);?
相關參數的說明:第一個參數是要執行程序的名字,第二個參數是可變參數列表,表示你要如何執行,也是以NULL結尾。
寫一個執行ls程序:
execle("ls", "ls", "-l", NULL);
3??int execle(const char *path,?const char *arg,..., char *const envp[]);?
相關參數的說明:第一個參數是要執行的程序的路徑,第二個參數是可變參數列表,表示如何執行這個程序,也是以NULL結尾。第三個參數是自己設置的環境變量。
這里我們可以設置MYVAL的環境變量,在test中可以使用這個環境變量了。
char* myenp[] = {"MYVAL=2025", NULL};
execle("./test", "test", NULL, myenvp);
4??int execv(const char *path, char *const argv]);?
相關參數的說明:第一個參數是要執行程序的路徑,第二個參數是一個指針數組,數組中的就是你要如何執行這個程序,數組也是以NULL結尾的。
寫一個ls程序:
char* myargv[] = {"ls", "-l", NULL);
execv("/usr/bin/ls", myargv);
5??int execvp(const char *file,char *const argv[]);
相關參數的說明:第一個是要執行的程序的名字,第二個參數是一個指針數組,?數組中的就是你要如何執行這個程序,數組也是以NULL結尾的。
寫一個ls程序:
char* myargv[] = {"ls", "-l", NULL};
execvp("ls", myargv);
6??int execve(const char *path,? char *const argvl],? char *const envp[]);
相關參數的說明:?第一個參數是要執行程序的路徑,第二個參數是一個指針數組,數組中的就是你要如何執行這個程序,數組也是以NULL結尾的,第三個參數是自己設置的環境變量。
這里我們可以設置MYVAL的環境變量,在test中可以使用這個環境變量了。
char* myargv[] = {"mycmd", NULL};
char* myenvp[] = {"MYVAL=2025", NULL};
execve("./test", test, myenvp);
3)函數解釋?
- 這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回。
- 如果調用出錯則返回-1
所以exec函數只有出錯的返回值而沒有成功的返回值。
4)命名理解?
其實我們仔細觀察就會發現這六個函數都是有規律的,只要掌握了規律是很好記的。
- l(list):表示參數采用列表的形式
- v(vector):參數用數組的形式
- p(path):有p自動搜索環境變量PATH
- e(env):表示自己維護環境變量
函數名 | 參數格式 | 是否帶路徑 | 是否使用當前環境變量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,須自己組裝環境變量 |
execv | 數組 | 不是 | 是 |
execvp | 數組 | 是 | 是 |
execve | 數組 | 不是 | 不是,須自己組裝環境變量 |
事實上,我們打開man手冊就可以發現execve在man手冊第2節其它函數在man手冊第3節。也就是 只有execve是真正的系統調用,其它五個函數最終都調用execve。