三、進程等待
進程等待的必要性
- 子進程退出,父進程如果不讀取子進程的退出信息,子進程就會變成僵尸進程,進而造成內(nèi)存泄漏。
- 進程一旦變成僵尸進程,那么就算是kill -9命令也無法將其殺死,因為誰也無法殺死一個已經(jīng)死去的進程。
- 對于一個進程來說,最關(guān)心自己的就是其父進程,因為父進程需要知道自己派給子進程的任務(wù)完成的如何。
- 父進程需要通過進程等待的方式,回收子進程資源,獲取子進程的退出信息。
獲取子進程status
下面進程等待所使用的兩個函數(shù)wait和waitpid,都有一個status參數(shù),該參數(shù)是一個輸出型參數(shù),由操作系統(tǒng)進行填充。
如果對status參數(shù)傳入NULL,表示不關(guān)心子進程的退出狀態(tài)信息。否則,操作系統(tǒng)會通過該參數(shù),將子進程的退出信息反饋給父進程。
status是一個整型變量,但status不能簡單的當作整型來看待,status的不同比特位所代表的信息不同,具體細節(jié)如下(只研究status低16比特位):

在status的低16比特位當中,高8位表示進程的退出狀態(tài),即退出碼。進程若是被信號所殺,則低7位表示終止信號,而第8位比特位是core dump標志。

我們通過一系列位操作,就可以根據(jù)status得到進程的退出碼和退出信號。
exitCode = (status >> 8) & 0xFF; //退出碼
exitSignal = status & 0x7F; //退出信號
對于此,系統(tǒng)當中提供了兩個宏來獲取退出碼和退出信號。
- WIFEXITED(status):用于查看進程是否是正常退出,本質(zhì)是檢查是否收到信號。
- WEXITSTATUS(status):用于獲取進程的退出碼。
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //獲取退出碼
需要注意的是,當一個進程非正常退出時,說明該進程是被信號所殺,那么該進程的退出碼也就沒有意義了。
進程等待的方法
wait方法
函數(shù)原型:pid_t wait(int* status);
作用:等待任意子進程。
返回值:等待成功返回被等待進程的pid,等待失敗返回-1。
參數(shù):輸出型參數(shù),獲取子進程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。
例如,創(chuàng)建子進程后,父進程可使用wait函數(shù)一直等待子進程,直到子進程退出后讀取子進程的退出信息。
#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;
}
我們可以使用以下監(jiān)控腳本對進程進行實時監(jiān)控:
[cl@VM-0-15-centos procWait]$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
這時我們可以看到,當子進程退出后,父進程讀取了子進程的退出信息,子進程也就不會變成僵尸進程了。

waitpid方法
函數(shù)原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子進程或任意子進程。
返回值:
1、等待成功返回被等待進程的pid。
2、如果設(shè)置了選項WNOHANG,而調(diào)用中waitpid發(fā)現(xiàn)沒有已退出的子進程可收集,則返回0。
3、如果調(diào)用中出錯,則返回-1,這時errno會被設(shè)置成相應(yīng)的值以指示錯誤所在。
參數(shù):
1、pid:待等待子進程的pid,若設(shè)置為-1,則等待任意子進程。
2、status:輸出型參數(shù),獲取子進程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。
3、options:當設(shè)置為WNOHANG時,若等待的子進程沒有結(jié)束,則waitpid函數(shù)直接返回0,不予以等待。若正常結(jié)束,則返回該子進程的pid。
例如,創(chuàng)建子進程后,父進程可使用waitpid函數(shù)一直等待子進程(此時將waitpid的第三個參數(shù)設(shè)置為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命令將子進程殺死,這時父進程也能等待子進程成功。

注意: 被信號殺死而退出的進程,其退出碼將沒有意義。
多進程創(chuàng)建以及等待的代碼模型
上面演示的都是父進程創(chuàng)建以及等待一個子進程的例子,實際上我們還可以同時創(chuàng)建多個子進程,然后讓父進程依次等待子進程退出,這叫做多進程創(chuàng)建以及等待的代碼模型。
例如,以下代碼中同時創(chuàng)建了10個子進程,同時將子進程的pid放入到ids數(shù)組當中,并將這10個子進程退出時的退出碼設(shè)置為該子進程pid在數(shù)組ids中的下標,之后父進程再使用waitpid函數(shù)指定等待這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); //將子進程的退出碼設(shè)置為該子進程PID在數(shù)組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;
}
運行代碼,這時我們便可以看到父進程同時創(chuàng)建多個子進程,當子進程退出后,父進程再依次讀取這些子進程的退出信息。
基于非阻塞接口的輪詢檢測方案
上述所給例子中,當子進程未退出時,父進程都在一直等待子進程退出,在等待期間,父進程不能做任何事情,這種等待叫做阻塞等待。
實際上我們可以讓父進程不要一直等待子進程退出,而是當子進程未退出時父進程可以做一些自己的事情,當子進程退出時再讀取子進程的退出信息,即非阻塞等待。
做法很簡單,向waitpid函數(shù)的第三個參數(shù)potions傳入WNOHANG,這樣一來,等待的子進程若是沒有結(jié)束,那么waitpid函數(shù)將直接返回0,不予以等待。而等待的子進程若是正常結(jié)束,則返回該子進程的pid。
例如,父進程可以隔一段時間調(diào)用一次waitpid函數(shù),若是等待的子進程尚未退出,則父進程可以先去做一些其他事,過一段時間再調(diào)用waitpid函數(shù)讀取子進程的退出信息。
#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;
}
運行結(jié)果就是,父進程每隔一段時間就去查看子進程是否退出,若未退出,則父進程先去忙自己的事情,過一段時間再來查看,直到子進程退出后讀取子進程的退出信息。

四、進程程序替換
替換原理
用fork創(chuàng)建子進程后,子進程執(zhí)行的是和父進程相同的程序(但有可能執(zhí)行不同的代碼分支),若想讓子進程執(zhí)行另一個程序,往往需要調(diào)用一種exec函數(shù)。
當進程調(diào)用一種exec函數(shù)時,該進程的用戶空間代碼和數(shù)據(jù)完全被新程序替換,并從新程序的啟動例程開始執(zhí)行。
當進行進程程序替換時,有沒有創(chuàng)建新的進程?
進程程序替換之后,該進程對應(yīng)的PCB、進程地址空間以及頁表等數(shù)據(jù)結(jié)構(gòu)都沒有發(fā)生改變,只是進程在物理內(nèi)存當中的數(shù)據(jù)和代碼發(fā)生了改變,所以并沒有創(chuàng)建新的進程,而且進程程序替換前后該進程的pid并沒有改變。
子進程進行進程程序替換后,會影響父進程的代碼和數(shù)據(jù)嗎?
子進程剛被創(chuàng)建時,與父進程共享代碼和數(shù)據(jù),但當子進程需要進行進程程序替換時,也就意味著子進程需要對其數(shù)據(jù)和代碼進行寫入操作,這時便需要將父子進程共享的代碼和數(shù)據(jù)進行寫時拷貝,此后父子進程的代碼和數(shù)據(jù)也就分離了,因此子進程進行程序替換后不會影響父進程的代碼和數(shù)據(jù)。
替換函數(shù)
替換函數(shù)有六種以exec開頭的函數(shù),它們統(tǒng)稱為exec函數(shù):
一、int execl(const char *path, const char *arg, ...);
第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個程序,并以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
二、int execlp(const char *file, const char *arg, ...);
第一個參數(shù)是要執(zhí)行程序的名字,第二個參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個程序,并以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
execlp("ls", "ls", "-a", "-i", "-l", NULL);
三、int execle(const char *path, const char *arg, ..., char *const envp[]);
第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個程序,并以NULL結(jié)尾,第三個參數(shù)是你自己設(shè)置的環(huán)境變量。
例如,你設(shè)置了MYVAL環(huán)境變量,在mycmd程序內(nèi)部就可以使用該環(huán)境變量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
四、int execv(const char *path, char *const argv[]);
第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是一個指針數(shù)組,數(shù)組當中的內(nèi)容表示你要如何執(zhí)行這個程序,數(shù)組以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
五、int execvp(const char *file, char *const argv[]);
第一個參數(shù)是要執(zhí)行程序的名字,第二個參數(shù)是一個指針數(shù)組,數(shù)組當中的內(nèi)容表示你要如何執(zhí)行這個程序,數(shù)組以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
六、int execve(const char *path, char *const argv[], char *const envp[]);
第一個參數(shù)是要執(zhí)行程序的路徑,第二個參數(shù)是一個指針數(shù)組,數(shù)組當中的內(nèi)容表示你要如何執(zhí)行這個程序,數(shù)組以NULL結(jié)尾,第三個參數(shù)是你自己設(shè)置的環(huán)境變量。
例如,你設(shè)置了MYVAL環(huán)境變量,在mycmd程序內(nèi)部就可以使用該環(huán)境變量。
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);
函數(shù)解釋
- 這些函數(shù)如果調(diào)用成功,則加載指定的程序并從啟動代碼開始執(zhí)行,不再返回。
- 如果調(diào)用出錯,則返回-1。
也就是說,exec系列函數(shù)只要返回了,就意味著調(diào)用失敗。
命名理解
這六個exec系列函數(shù)的函數(shù)名都以exec開頭,其后綴的含義如下:
- l(list):表示參數(shù)采用列表的形式,一一列出。
- v(vector):表示參數(shù)采用數(shù)組的形式。
- p(path):表示能自動搜索環(huán)境變量PATH,進行程序查找。
- e(env):表示可以傳入自己設(shè)置的環(huán)境變量。

事實上,只有execve才是真正的系統(tǒng)調(diào)用,其它五個函數(shù)最終都是調(diào)用的execve,所以execve在man手冊的第2節(jié),而其它五個函數(shù)在man手冊的第3節(jié),也就是說其他五個函數(shù)實際上是對系統(tǒng)調(diào)用execve進行了封裝,以滿足不同用戶的不同調(diào)用場景的。
下圖為exec系列函數(shù)族之間的關(guān)系:

做一個簡易的shell
shell也就是命令行解釋器,其運行原理就是:當有命令需要執(zhí)行時,shell創(chuàng)建子進程,讓子進程執(zhí)行命令,而shell只需等待子進程退出即可。

其實shell需要執(zhí)行的邏輯非常簡單,其只需循環(huán)執(zhí)行以下步驟:
- 獲取命令行。
- 解析命令行。
- 創(chuàng)建子進程。
- 替換子進程。
- 等待子進程退出。
其中,創(chuàng)建子進程使用fork函數(shù),替換子進程使用exec系列函數(shù),等待子進程使用wait或者waitpid函數(shù)。
于是我們可以很容易實現(xiàn)一個簡易的shell,代碼如下:
#include
#include
#include
#include
#include
#include
#include
#define LEN 1024 //命令最大長度
#define NUM 32 //命令拆分后的最大個數(shù)
int main()
{
char cmd[LEN]; //存儲命令
char* myargv[NUM]; //存儲命令拆分后的結(jié)果
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(); //創(chuàng)建子進程執(zhí)行命令
if (id == 0){
//child
execvp(myargv[0], myargv); //child進行程序替換
exit(1); //替換失敗的退出碼設(shè)置為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;
}
效果展示:

說明:
當執(zhí)行./myshell命令后,便是我們自己實現(xiàn)的shell在進行命令行解釋,我們自己實現(xiàn)的shell在子進程退出后都打印了子進程的退出碼,我們可以根據(jù)這一點來區(qū)分我們當前使用的是Linux操作系統(tǒng)的shell還是我們自己實現(xiàn)的shell。
-
Linux
+關(guān)注
關(guān)注
88文章
11581瀏覽量
217154 -
PID
+關(guān)注
關(guān)注
37文章
1487瀏覽量
89740 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4401瀏覽量
66536 -
數(shù)據(jù)結(jié)構(gòu)
+關(guān)注
關(guān)注
3文章
573瀏覽量
41231
發(fā)布評論請先 登錄
Linux中進程和線程的深度對比

深度剖析Linux中進程控制(下)
評論