三、進(jìn)程等待
進(jìn)程等待的必要性
- 子進(jìn)程退出,父進(jìn)程如果不讀取子進(jìn)程的退出信息,子進(jìn)程就會(huì)變成僵尸進(jìn)程,進(jìn)而造成內(nèi)存泄漏。
- 進(jìn)程一旦變成僵尸進(jìn)程,那么就算是kill -9命令也無(wú)法將其殺死,因?yàn)檎l(shuí)也無(wú)法殺死一個(gè)已經(jīng)死去的進(jìn)程。
- 對(duì)于一個(gè)進(jìn)程來說,最關(guān)心自己的就是其父進(jìn)程,因?yàn)楦高M(jìn)程需要知道自己派給子進(jìn)程的任務(wù)完成的如何。
- 父進(jìn)程需要通過進(jìn)程等待的方式,回收子進(jìn)程資源,獲取子進(jìn)程的退出信息。
獲取子進(jìn)程status
下面進(jìn)程等待所使用的兩個(gè)函數(shù)wait和waitpid,都有一個(gè)status參數(shù),該參數(shù)是一個(gè)輸出型參數(shù),由操作系統(tǒng)進(jìn)行填充。
如果對(duì)status參數(shù)傳入NULL,表示不關(guān)心子進(jìn)程的退出狀態(tài)信息。否則,操作系統(tǒng)會(huì)通過該參數(shù),將子進(jìn)程的退出信息反饋給父進(jìn)程。
status是一個(gè)整型變量,但status不能簡(jiǎn)單的當(dāng)作整型來看待,status的不同比特位所代表的信息不同,具體細(xì)節(jié)如下(只研究status低16比特位):
在status的低16比特位當(dāng)中,高8位表示進(jìn)程的退出狀態(tài),即退出碼。進(jìn)程若是被信號(hào)所殺,則低7位表示終止信號(hào),而第8位比特位是core dump標(biāo)志。
我們通過一系列位操作,就可以根據(jù)status得到進(jìn)程的退出碼和退出信號(hào)。
exitCode = (status >> 8) & 0xFF; //退出碼
exitSignal = status & 0x7F; //退出信號(hào)
對(duì)于此,系統(tǒng)當(dāng)中提供了兩個(gè)宏來獲取退出碼和退出信號(hào)。
- WIFEXITED(status):用于查看進(jìn)程是否是正常退出,本質(zhì)是檢查是否收到信號(hào)。
- WEXITSTATUS(status):用于獲取進(jìn)程的退出碼。
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //獲取退出碼
需要注意的是,當(dāng)一個(gè)進(jìn)程非正常退出時(shí),說明該進(jìn)程是被信號(hào)所殺,那么該進(jìn)程的退出碼也就沒有意義了。
進(jìn)程等待的方法
wait方法
函數(shù)原型:pid_t wait(int* status);
作用:等待任意子進(jìn)程。
返回值:等待成功返回被等待進(jìn)程的pid,等待失敗返回-1。
參數(shù):輸出型參數(shù),獲取子進(jìn)程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。
例如,創(chuàng)建子進(jìn)程后,父進(jìn)程可使用wait函數(shù)一直等待子進(jìn)程,直到子進(jìn)程退出后讀取子進(jìn)程的退出信息。
#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)控腳本對(duì)進(jìn)程進(jìn)行實(shí)時(shí)監(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
這時(shí)我們可以看到,當(dāng)子進(jìn)程退出后,父進(jìn)程讀取了子進(jìn)程的退出信息,子進(jìn)程也就不會(huì)變成僵尸進(jìn)程了。
waitpid方法
函數(shù)原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子進(jìn)程或任意子進(jìn)程。
返回值:
1、等待成功返回被等待進(jìn)程的pid。
2、如果設(shè)置了選項(xiàng)WNOHANG,而調(diào)用中waitpid發(fā)現(xiàn)沒有已退出的子進(jìn)程可收集,則返回0。
3、如果調(diào)用中出錯(cuò),則返回-1,這時(shí)errno會(huì)被設(shè)置成相應(yīng)的值以指示錯(cuò)誤所在。
參數(shù):
1、pid:待等待子進(jìn)程的pid,若設(shè)置為-1,則等待任意子進(jìn)程。
2、status:輸出型參數(shù),獲取子進(jìn)程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。
3、options:當(dāng)設(shè)置為WNOHANG時(shí),若等待的子進(jìn)程沒有結(jié)束,則waitpid函數(shù)直接返回0,不予以等待。若正常結(jié)束,則返回該子進(jìn)程的pid。
例如,創(chuàng)建子進(jìn)程后,父進(jìn)程可使用waitpid函數(shù)一直等待子進(jìn)程(此時(shí)將waitpid的第三個(gè)參數(shù)設(shè)置為0),直到子進(jìn)程退出后讀取子進(jìn)程的退出信息。
#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;
}
在父進(jìn)程運(yùn)行過程中,我們可以嘗試使用kill -9命令將子進(jìn)程殺死,這時(shí)父進(jìn)程也能等待子進(jìn)程成功。
注意: 被信號(hào)殺死而退出的進(jìn)程,其退出碼將沒有意義。
多進(jìn)程創(chuàng)建以及等待的代碼模型
上面演示的都是父進(jìn)程創(chuàng)建以及等待一個(gè)子進(jìn)程的例子,實(shí)際上我們還可以同時(shí)創(chuàng)建多個(gè)子進(jìn)程,然后讓父進(jìn)程依次等待子進(jìn)程退出,這叫做多進(jìn)程創(chuàng)建以及等待的代碼模型。
例如,以下代碼中同時(shí)創(chuàng)建了10個(gè)子進(jìn)程,同時(shí)將子進(jìn)程的pid放入到ids數(shù)組當(dāng)中,并將這10個(gè)子進(jìn)程退出時(shí)的退出碼設(shè)置為該子進(jìn)程pid在數(shù)組ids中的下標(biāo),之后父進(jìn)程再使用waitpid函數(shù)指定等待這10個(gè)子進(jìn)程。
#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); //將子進(jìn)程的退出碼設(shè)置為該子進(jìn)程PID在數(shù)組ids中的下標(biāo)
}
//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;
}
運(yùn)行代碼,這時(shí)我們便可以看到父進(jìn)程同時(shí)創(chuàng)建多個(gè)子進(jìn)程,當(dāng)子進(jìn)程退出后,父進(jìn)程再依次讀取這些子進(jìn)程的退出信息。
基于非阻塞接口的輪詢檢測(cè)方案
上述所給例子中,當(dāng)子進(jìn)程未退出時(shí),父進(jìn)程都在一直等待子進(jìn)程退出,在等待期間,父進(jìn)程不能做任何事情,這種等待叫做阻塞等待。
實(shí)際上我們可以讓父進(jìn)程不要一直等待子進(jìn)程退出,而是當(dāng)子進(jìn)程未退出時(shí)父進(jìn)程可以做一些自己的事情,當(dāng)子進(jìn)程退出時(shí)再讀取子進(jìn)程的退出信息,即非阻塞等待。
做法很簡(jiǎn)單,向waitpid函數(shù)的第三個(gè)參數(shù)potions傳入WNOHANG,這樣一來,等待的子進(jìn)程若是沒有結(jié)束,那么waitpid函數(shù)將直接返回0,不予以等待。而等待的子進(jìn)程若是正常結(jié)束,則返回該子進(jìn)程的pid。
例如,父進(jìn)程可以隔一段時(shí)間調(diào)用一次waitpid函數(shù),若是等待的子進(jìn)程尚未退出,則父進(jìn)程可以先去做一些其他事,過一段時(shí)間再調(diào)用waitpid函數(shù)讀取子進(jìn)程的退出信息。
#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;
}
運(yùn)行結(jié)果就是,父進(jìn)程每隔一段時(shí)間就去查看子進(jìn)程是否退出,若未退出,則父進(jìn)程先去忙自己的事情,過一段時(shí)間再來查看,直到子進(jìn)程退出后讀取子進(jìn)程的退出信息。
四、進(jìn)程程序替換
替換原理
用fork創(chuàng)建子進(jìn)程后,子進(jìn)程執(zhí)行的是和父進(jìn)程相同的程序(但有可能執(zhí)行不同的代碼分支),若想讓子進(jìn)程執(zhí)行另一個(gè)程序,往往需要調(diào)用一種exec函數(shù)。
當(dāng)進(jìn)程調(diào)用一種exec函數(shù)時(shí),該進(jìn)程的用戶空間代碼和數(shù)據(jù)完全被新程序替換,并從新程序的啟動(dòng)例程開始執(zhí)行。
當(dāng)進(jìn)行進(jìn)程程序替換時(shí),有沒有創(chuàng)建新的進(jìn)程?
進(jìn)程程序替換之后,該進(jìn)程對(duì)應(yīng)的PCB、進(jìn)程地址空間以及頁(yè)表等數(shù)據(jù)結(jié)構(gòu)都沒有發(fā)生改變,只是進(jìn)程在物理內(nèi)存當(dāng)中的數(shù)據(jù)和代碼發(fā)生了改變,所以并沒有創(chuàng)建新的進(jìn)程,而且進(jìn)程程序替換前后該進(jìn)程的pid并沒有改變。
子進(jìn)程進(jìn)行進(jìn)程程序替換后,會(huì)影響父進(jìn)程的代碼和數(shù)據(jù)嗎?
子進(jìn)程剛被創(chuàng)建時(shí),與父進(jìn)程共享代碼和數(shù)據(jù),但當(dāng)子進(jìn)程需要進(jìn)行進(jìn)程程序替換時(shí),也就意味著子進(jìn)程需要對(duì)其數(shù)據(jù)和代碼進(jìn)行寫入操作,這時(shí)便需要將父子進(jìn)程共享的代碼和數(shù)據(jù)進(jìn)行寫時(shí)拷貝,此后父子進(jìn)程的代碼和數(shù)據(jù)也就分離了,因此子進(jìn)程進(jìn)行程序替換后不會(huì)影響父進(jìn)程的代碼和數(shù)據(jù)。
替換函數(shù)
替換函數(shù)有六種以exec開頭的函數(shù),它們統(tǒng)稱為exec函數(shù):
一、int execl(const char *path, const char *arg, ...);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個(gè)程序,并以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
二、int execlp(const char *file, const char *arg, ...);
第一個(gè)參數(shù)是要執(zhí)行程序的名字,第二個(gè)參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個(gè)程序,并以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
execlp("ls", "ls", "-a", "-i", "-l", NULL);
三、int execle(const char *path, const char *arg, ..., char *const envp[]);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個(gè)程序,并以NULL結(jié)尾,第三個(gè)參數(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[]);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是一個(gè)指針數(shù)組,數(shù)組當(dāng)中的內(nèi)容表示你要如何執(zhí)行這個(gè)程序,數(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[]);
第一個(gè)參數(shù)是要執(zhí)行程序的名字,第二個(gè)參數(shù)是一個(gè)指針數(shù)組,數(shù)組當(dāng)中的內(nèi)容表示你要如何執(zhí)行這個(gè)程序,數(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[]);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是一個(gè)指針數(shù)組,數(shù)組當(dāng)中的內(nèi)容表示你要如何執(zhí)行這個(gè)程序,數(shù)組以NULL結(jié)尾,第三個(gè)參數(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)用成功,則加載指定的程序并從啟動(dòng)代碼開始執(zhí)行,不再返回。
- 如果調(diào)用出錯(cuò),則返回-1。
也就是說,exec系列函數(shù)只要返回了,就意味著調(diào)用失敗。
命名理解
這六個(gè)exec系列函數(shù)的函數(shù)名都以exec開頭,其后綴的含義如下:
- l(list):表示參數(shù)采用列表的形式,一一列出。
- v(vector):表示參數(shù)采用數(shù)組的形式。
- p(path):表示能自動(dòng)搜索環(huán)境變量PATH,進(jìn)行程序查找。
- e(env):表示可以傳入自己設(shè)置的環(huán)境變量。
事實(shí)上,只有execve才是真正的系統(tǒng)調(diào)用,其它五個(gè)函數(shù)最終都是調(diào)用的execve,所以execve在man手冊(cè)的第2節(jié),而其它五個(gè)函數(shù)在man手冊(cè)的第3節(jié),也就是說其他五個(gè)函數(shù)實(shí)際上是對(duì)系統(tǒng)調(diào)用execve進(jìn)行了封裝,以滿足不同用戶的不同調(diào)用場(chǎng)景的。
下圖為exec系列函數(shù)族之間的關(guān)系:
做一個(gè)簡(jiǎn)易的shell
shell也就是命令行解釋器,其運(yùn)行原理就是:當(dāng)有命令需要執(zhí)行時(shí),shell創(chuàng)建子進(jìn)程,讓子進(jìn)程執(zhí)行命令,而shell只需等待子進(jìn)程退出即可。
其實(shí)shell需要執(zhí)行的邏輯非常簡(jiǎn)單,其只需循環(huán)執(zhí)行以下步驟:
- 獲取命令行。
- 解析命令行。
- 創(chuàng)建子進(jìn)程。
- 替換子進(jìn)程。
- 等待子進(jìn)程退出。
其中,創(chuàng)建子進(jìn)程使用fork函數(shù),替換子進(jìn)程使用exec系列函數(shù),等待子進(jìn)程使用wait或者waitpid函數(shù)。
于是我們可以很容易實(shí)現(xiàn)一個(gè)簡(jiǎn)易的shell,代碼如下:
#include
#include
#include
#include
#include
#include
#include
#define LEN 1024 //命令最大長(zhǎng)度
#define NUM 32 //命令拆分后的最大個(gè)數(shù)
int main()
{
char cmd[LEN]; //存儲(chǔ)命令
char* myargv[NUM]; //存儲(chǔ)命令拆分后的結(jié)果
char hostname[32]; //主機(jī)名
char pwd[128]; //當(dāng)前目錄
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)建子進(jìn)程執(zhí)行命令
if (id == 0){
//child
execvp(myargv[0], myargv); //child進(jìn)行程序替換
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;
}
效果展示:
說明:
當(dāng)執(zhí)行./myshell命令后,便是我們自己實(shí)現(xiàn)的shell在進(jìn)行命令行解釋,我們自己實(shí)現(xiàn)的shell在子進(jìn)程退出后都打印了子進(jìn)程的退出碼,我們可以根據(jù)這一點(diǎn)來區(qū)分我們當(dāng)前使用的是Linux操作系統(tǒng)的shell還是我們自己實(shí)現(xiàn)的shell。
-
Linux
+關(guān)注
關(guān)注
87文章
11227瀏覽量
208922 -
PID
+關(guān)注
關(guān)注
35文章
1471瀏覽量
85287 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4304瀏覽量
62429 -
數(shù)據(jù)結(jié)構(gòu)
+關(guān)注
關(guān)注
3文章
573瀏覽量
40092
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論