三、進程等待
進程等待的必要性
- 子進程退出,父進程如果不讀取子進程的退出信息,子進程就會變成僵尸進程,進而造成內存泄漏。
- 進程一旦變成僵尸進程,那么就算是kill -9命令也無法將其殺死,因為誰也無法殺死一個已經死去的進程。
- 對于一個進程來說,最關心自己的就是其父進程,因為父進程需要知道自己派給子進程的任務完成的如何。
- 父進程需要通過進程等待的方式,回收子進程資源,獲取子進程的退出信息。
獲取子進程status
下面進程等待所使用的兩個函數wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統進行填充。
如果對status參數傳入NULL,表示不關心子進程的退出狀態信息。否則,操作系統會通過該參數,將子進程的退出信息反饋給父進程。
status是一個整型變量,但status不能簡單的當作整型來看待,status的不同比特位所代表的信息不同,具體細節如下(只研究status低16比特位):
在status的低16比特位當中,高8位表示進程的退出狀態,即退出碼。進程若是被信號所殺,則低7位表示終止信號,而第8位比特位是core dump標志。
我們通過一系列位操作,就可以根據status得到進程的退出碼和退出信號。
exitCode = (status >> 8) & 0xFF; //退出碼
exitSignal = status & 0x7F; //退出信號
對于此,系統當中提供了兩個宏來獲取退出碼和退出信號。
- WIFEXITED(status):用于查看進程是否是正常退出,本質是檢查是否收到信號。
- WEXITSTATUS(status):用于獲取進程的退出碼。
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //獲取退出碼
需要注意的是,當一個進程非正常退出時,說明該進程是被信號所殺,那么該進程的退出碼也就沒有意義了。
進程等待的方法
wait方法
函數原型:pid_t wait(int* status);
作用:等待任意子進程。
返回值:等待成功返回被等待進程的pid,等待失敗返回-1。
參數:輸出型參數,獲取子進程的退出狀態,不關心可設置為NULL。
例如,創建子進程后,父進程可使用wait函數一直等待子進程,直到子進程退出后讀取子進程的退出信息。
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0){
//child
int count = 10;
while(count--){
printf("I am child...PID:%d, PPID:%d\\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(&status);
if(ret > 0){
//wait success
printf("wait child success...\\n");
if(WIFEXITED(status)){
//exit normal
printf("exit code:%d\\n", WEXITSTATUS(status));
}
}
sleep(3);
return 0;
}
我們可以使用以下監控腳本對進程進行實時監控:
[cl@VM-0-15-centos procWait]$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
這時我們可以看到,當子進程退出后,父進程讀取了子進程的退出信息,子進程也就不會變成僵尸進程了。
waitpid方法
函數原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子進程或任意子進程。
返回值:
1、等待成功返回被等待進程的pid。
2、如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0。
3、如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在。
參數:
1、pid:待等待子進程的pid,若設置為-1,則等待任意子進程。
2、status:輸出型參數,獲取子進程的退出狀態,不關心可設置為NULL。
3、options:當設置為WNOHANG時,若等待的子進程沒有結束,則waitpid函數直接返回0,不予以等待。若正常結束,則返回該子進程的pid。
例如,創建子進程后,父進程可使用waitpid函數一直等待子進程(此時將waitpid的第三個參數設置為0),直到子進程退出后讀取子進程的退出信息。
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 10;
while (count--){
printf("I am child...PID:%d, PPID:%d\\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret >= 0){
//wait success
printf("wait child success...\\n");
if (WIFEXITED(status)){
//exit normal
printf("exit code:%d\\n", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by siganl %d\\n", status & 0x7F);
}
}
sleep(3);
return 0;
}
在父進程運行過程中,我們可以嘗試使用kill -9命令將子進程殺死,這時父進程也能等待子進程成功。
注意: 被信號殺死而退出的進程,其退出碼將沒有意義。
多進程創建以及等待的代碼模型
上面演示的都是父進程創建以及等待一個子進程的例子,實際上我們還可以同時創建多個子進程,然后讓父進程依次等待子進程退出,這叫做多進程創建以及等待的代碼模型。
例如,以下代碼中同時創建了10個子進程,同時將子進程的pid放入到ids數組當中,并將這10個子進程退出時的退出碼設置為該子進程pid在數組ids中的下標,之后父進程再使用waitpid函數指定等待這10個子進程。
#include
#include
#include
#include
#include
int main()
{
pid_t ids[10];
for (int i = 0; i < 10; i++){
pid_t id = fork();
if (id == 0){
//child
printf("child process created successfully...PID:%d\\n", getpid());
sleep(3);
exit(i); //將子進程的退出碼設置為該子進程PID在數組ids中的下標
}
//father
ids[i] = id;
}
for (int i = 0; i < 10; i++){
int status = 0;
pid_t ret = waitpid(ids[i], &status, 0);
if (ret >= 0){
//wait child success
printf("wiat child success..PID:%d\\n", ids[i]);
if (WIFEXITED(status)){
//exit normal
printf("exit code:%d\\n", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by signal %d\\n", status & 0x7F);
}
}
}
return 0;
}
運行代碼,這時我們便可以看到父進程同時創建多個子進程,當子進程退出后,父進程再依次讀取這些子進程的退出信息。
基于非阻塞接口的輪詢檢測方案
上述所給例子中,當子進程未退出時,父進程都在一直等待子進程退出,在等待期間,父進程不能做任何事情,這種等待叫做阻塞等待。
實際上我們可以讓父進程不要一直等待子進程退出,而是當子進程未退出時父進程可以做一些自己的事情,當子進程退出時再讀取子進程的退出信息,即非阻塞等待。
做法很簡單,向waitpid函數的第三個參數potions傳入WNOHANG,這樣一來,等待的子進程若是沒有結束,那么waitpid函數將直接返回0,不予以等待。而等待的子進程若是正常結束,則返回該子進程的pid。
例如,父進程可以隔一段時間調用一次waitpid函數,若是等待的子進程尚未退出,則父進程可以先去做一些其他事,過一段時間再調用waitpid函數讀取子進程的退出信息。
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 3;
while (count--){
printf("child do something...PID:%d, PPID:%d\\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1){
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0){
printf("wait child success...\\n");
printf("exit code:%d\\n", WEXITSTATUS(status));
break;
}
else if (ret == 0){
printf("father do other things...\\n");
sleep(1);
}
else{
printf("waitpid error...\\n");
break;
}
}
return 0;
}
運行結果就是,父進程每隔一段時間就去查看子進程是否退出,若未退出,則父進程先去忙自己的事情,過一段時間再來查看,直到子進程退出后讀取子進程的退出信息。
四、進程程序替換
替換原理
用fork創建子進程后,子進程執行的是和父進程相同的程序(但有可能執行不同的代碼分支),若想讓子進程執行另一個程序,往往需要調用一種exec函數。
當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,并從新程序的啟動例程開始執行。
當進行進程程序替換時,有沒有創建新的進程?
進程程序替換之后,該進程對應的PCB、進程地址空間以及頁表等數據結構都沒有發生改變,只是進程在物理內存當中的數據和代碼發生了改變,所以并沒有創建新的進程,而且進程程序替換前后該進程的pid并沒有改變。
子進程進行進程程序替換后,會影響父進程的代碼和數據嗎?
子進程剛被創建時,與父進程共享代碼和數據,但當子進程需要進行進程程序替換時,也就意味著子進程需要對其數據和代碼進行寫入操作,這時便需要將父子進程共享的代碼和數據進行寫時拷貝,此后父子進程的代碼和數據也就分離了,因此子進程進行程序替換后不會影響父進程的代碼和數據。
替換函數
替換函數有六種以exec開頭的函數,它們統稱為exec函數:
一、int execl(const char *path, const char *arg, ...);
第一個參數是要執行程序的路徑,第二個參數是可變參數列表,表示你要如何執行這個程序,并以NULL結尾。
例如,要執行的是ls程序。
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
二、int execlp(const char *file, const char *arg, ...);
第一個參數是要執行程序的名字,第二個參數是可變參數列表,表示你要如何執行這個程序,并以NULL結尾。
例如,要執行的是ls程序。
execlp("ls", "ls", "-a", "-i", "-l", NULL);
三、int execle(const char *path, const char *arg, ..., char *const envp[]);
第一個參數是要執行程序的路徑,第二個參數是可變參數列表,表示你要如何執行這個程序,并以NULL結尾,第三個參數是你自己設置的環境變量。
例如,你設置了MYVAL環境變量,在mycmd程序內部就可以使用該環境變量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
四、int execv(const char *path, char *const argv[]);
第一個參數是要執行程序的路徑,第二個參數是一個指針數組,數組當中的內容表示你要如何執行這個程序,數組以NULL結尾。
例如,要執行的是ls程序。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
五、int execvp(const char *file, char *const argv[]);
第一個參數是要執行程序的名字,第二個參數是一個指針數組,數組當中的內容表示你要如何執行這個程序,數組以NULL結尾。
例如,要執行的是ls程序。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
六、int execve(const char *path, char *const argv[], char *const envp[]);
第一個參數是要執行程序的路徑,第二個參數是一個指針數組,數組當中的內容表示你要如何執行這個程序,數組以NULL結尾,第三個參數是你自己設置的環境變量。
例如,你設置了MYVAL環境變量,在mycmd程序內部就可以使用該環境變量。
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);
函數解釋
- 這些函數如果調用成功,則加載指定的程序并從啟動代碼開始執行,不再返回。
- 如果調用出錯,則返回-1。
也就是說,exec系列函數只要返回了,就意味著調用失敗。
命名理解
這六個exec系列函數的函數名都以exec開頭,其后綴的含義如下:
- l(list):表示參數采用列表的形式,一一列出。
- v(vector):表示參數采用數組的形式。
- p(path):表示能自動搜索環境變量PATH,進行程序查找。
- e(env):表示可以傳入自己設置的環境變量。
事實上,只有execve才是真正的系統調用,其它五個函數最終都是調用的execve,所以execve在man手冊的第2節,而其它五個函數在man手冊的第3節,也就是說其他五個函數實際上是對系統調用execve進行了封裝,以滿足不同用戶的不同調用場景的。
下圖為exec系列函數族之間的關系:
做一個簡易的shell
shell也就是命令行解釋器,其運行原理就是:當有命令需要執行時,shell創建子進程,讓子進程執行命令,而shell只需等待子進程退出即可。
其實shell需要執行的邏輯非常簡單,其只需循環執行以下步驟:
- 獲取命令行。
- 解析命令行。
- 創建子進程。
- 替換子進程。
- 等待子進程退出。
其中,創建子進程使用fork函數,替換子進程使用exec系列函數,等待子進程使用wait或者waitpid函數。
于是我們可以很容易實現一個簡易的shell,代碼如下:
#include
#include
#include
#include
#include
#include
#include
#define LEN 1024 //命令最大長度
#define NUM 32 //命令拆分后的最大個數
int main()
{
char cmd[LEN]; //存儲命令
char* myargv[NUM]; //存儲命令拆分后的結果
char hostname[32]; //主機名
char pwd[128]; //當前目錄
while (1){
//獲取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//讀取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '\\0';
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //創建子進程執行命令
if (id == 0){
//child
execvp(myargv[0], myargv); //child進行程序替換
exit(1); //替換失敗的退出碼設置為1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0){
printf("exit code:%d\\n", WEXITSTATUS(status)); //打印child的退出碼
}
}
return 0;
}
效果展示:
說明:
當執行./myshell命令后,便是我們自己實現的shell在進行命令行解釋,我們自己實現的shell在子進程退出后都打印了子進程的退出碼,我們可以根據這一點來區分我們當前使用的是Linux操作系統的shell還是我們自己實現的shell。
-
Linux
+關注
關注
87文章
11292瀏覽量
209333 -
PID
+關注
關注
35文章
1472瀏覽量
85480 -
函數
+關注
關注
3文章
4327瀏覽量
62573 -
數據結構
+關注
關注
3文章
573瀏覽量
40123
發布評論請先 登錄
相關推薦
評論