一、函數wait、waitpid
一個進程在終止時會關閉所有文件描述符,釋放在用戶空間釋放的內存,但它的PCB還保留著,內核在其中保存一些信息:如果是正常終止時則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個,這個進程的父進程可以調用wait或waitpid獲取這些信息,然后徹底清除這個進程,我們知道一個進程的退出狀態可以在shell用特殊變量$?查看,因為shell是它的父進程,當它終止時shell調用wait或waitpid得到它的退出狀態同時徹底清除這個進程。
?
1. wait函數原型:一次只能回收一個子進程
pid_t wait(int *status);
- ?當進程終止時,操作系統隱式回收機制會:1. 關閉所有的文件描述符 2. 釋放用戶空間分配的內存。內核PCB仍存在,其中保存該進程的退出狀態。(正常終止--------退出值;異常終止-------終止信號
?
2. 函數waitpid原型:
作用:同wait,但可指定pid進程清理,可以不阻塞(?一次只能回收一個子進程)
pid_t waitpid(pid_t pid, int *staloc, int options);
參數pid:
- pid == -1:回收任一子進程
- pid??>??0 :回收指定pid的進程
- pid == 0 :回收與父進程同一個進程組的任一個子進程
- pid < -1??:回收指定進程組內的任意子進程
參數 options:
- 設置為WNOHANG:函數不阻塞;
- 設置為0:函數阻塞。
3. 測試代碼
#include <stdio.h>
#include <unistd.h>
#include<sys/wait.h>int main(int argc, const char* argv[])
{pid_t pid = fork();if (pid > 0) // 父進程{ printf("parent process, pid = %d, ppid = %d\n", getpid(), getppid());int status;pid_t wpid = wait(&status);if (WIFEXITED(status)) printf("exit value: %d", WEXITSTATUS(status));if (WIFSIGNALED(status)) printf("exit by signal: %d\n", WTERMSIG(status)); //是否被信號殺死printf(" die child pid = %d\n", wpid);}else if(pid == 0) {sleep(1);printf("child process, pid = %d, ppid = %d\n", getpid(), getppid()); }return 9;
}
輸出結果:
?
二、孤兒進程、僵尸進程
- 孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,并由init進程對它們完成狀態收集工作。
- 僵尸進程:一個進程使用fork創建子進程,如果子進程退出,而父進程并沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中。這種進程稱之為僵死進程。
1. unix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息, 就可以得到。這種機制就是: 在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,占用的內存等。 但是仍然為其保留一定的信息(包括進程號the process ID,退出狀態the termination status of the process,運行時間the amount of CPU time taken by the process等)。直到父進程通過wait / waitpid來取時才釋放。 但這樣就導致了問題,如果進程不調用wait / waitpid的話,?那么保留的那段信息就不會釋放,其進程號就會一直被占用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因為沒有可用的進程號而導致系統不能產生新的進程. 此即為僵尸進程的危害,應當避免。
2.?孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了init進程身上,init進程就好像是一個民政局,專門負責處理孤兒進程的善后工作。每當出現一個孤兒進程的時候,內核就把孤 兒進程的父進程設置為init,而init進程會循環地wait()它的已經退出的子進程。這樣,當一個孤兒進程凄涼地結束了其生命周期的時候,init進程就會代表黨和政府出面處理它的一切善后工作。因此孤兒進程并不會有什么危害。
1.?僵尸進程測試
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>int main()
{pid_t pid;while (1){pid = fork();if (pid < 0){perror("fork error:");exit(1);}else if (pid == 0){printf("I am a child process.\nI am exiting.\n");exit(0); //子進程退出,成為僵尸進程}else{sleep(20); //父進程休眠20s繼續創建子進程continue;}}return 0;
}
輸出結果:
?
僵尸進程解決辦法
1) . 通過信號機制
子進程退出時向父進程發送SIGCHILD信號,父進程處理SIGCHILD信號。在信號處理函數中調用wait進行處理僵尸進程。測試程序如下所示:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>static void sig_child(int signo)
{pid_t pid;int stat;while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) //處理僵尸進程printf("child %d terminated.\n", pid);
}int main()
{pid_t pid;signal(SIGCHLD, sig_child); //創建捕捉子進程退出信號pid = fork();if (pid < 0){perror("fork error:");exit(1);}else if (pid == 0){printf("I am child process,pid id %d.I am exiting.\n", getpid());exit(0);}printf("I am father process.I will sleep two seconds\n"); //等待子進程先退出sleep(2);system("ps -o pid,ppid,state,tty,command"); //輸出進程信息printf("father process is exiting.\n");return 0;
}
輸出結果:?
?
2)fork兩次
《Unix 環境高級編程》8.6節說的非常詳細。原理是將子進程成為孤兒進程,從而其的父進程變為init進程,通過init進程可以處理僵尸進程。測試程序如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>int main()
{pid_t pid;pid = fork(); if (pid < 0){perror("fork error:");exit(1);}else if (pid == 0) //第一個子進程{printf("I am the first child process. pid:%d\tppid:%d\n", getpid(), getppid());pid = fork();if (pid < 0){perror("fork error:");exit(1);}else if (pid > 0) //第一個子進程退出{printf("first procee is exited.\n");exit(0);}//第二個子進程//睡眠3s保證第一個子進程退出,這樣第二個子進程的父親就是init進程里sleep(3);printf("I am the second child process. pid: %d\tppid:%d\n", getpid(), getppid());exit(0);}if (waitpid(pid, NULL, 0) != pid) //父進程處理第一個子進程退出{perror("waitepid error:");exit(1);}exit(0);return 0;
}
輸出結果:
?
1.?孤兒進程與僵尸進程[總結]
二、exec函數族
1. 簡介
- 進程程序替換原理
fork創建子進程執行的是和父進程相同的程序(也有可能是某個分支),通常fork出的子進程是為了完成父進程所分配的任務,所以子進程通常會調用一種exec函數(六種中的任何一種)來執行另一個任務。當進程調用exec函數時,當前用戶空間的代碼和數據會被新程序所替換,該進程就會從新程序的啟動歷程開始執行。在這個過程中沒有創建新進程,所以調用exec并沒有改變進程的id。
- 替換圖解(圖解)
?
(1). execl函數原型:
int execl(const char *path, const char *arg, ...);
?分析:
- path: 要執行的程序的絕對路徑
- 變參arg: 要執行的程序的需要的參數
- 第一arg:占位
- 后邊的arg: 命令的參數
- 參數寫完之后: NULL
- 一般執行自己寫的程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{printf("entering main process---\n");if(execl("ls","ls","-l",NULL)<0)perror("excl error");return 0;
}
輸出結果:
(2). execv函數原型:
int execv(const char *path, char *const argv[]);
分析:
- path = /bin/ps
- char* args[] = {"ps", "aux", NULL};
- execv("/bin/ps", args);
(3). execlp函數原型
int execlp(const char *file, const char *arg, ...);
分析:
- file: 執行的命令的名字
- 第一arg:占位
- 后邊的arg: 命令的參數
- 參數寫完之后: NULL
- 執行系統自帶的程序
-
execlp執行自定義的程序: file參數絕對路徑
(4). execvp函原型:
int execvp(const char *file, char *const argv[]);
(5). execle函數原型:
int execle(const char *path, const char *arg, ..., char *const envp[]);
分析:
- path: 執行的程序的絕對路徑??/home/itcast/a.out
- arg: 執行的的程序的參數
- envp: 用戶自己指定的搜索目錄, 替代PATH
- char* env[] = {"/home/itcast", "/bin", NULL};
int execve(const char *path, char *const argv[], char *const envp[]);
函數名 | 參數格式 | 是否帶路徑 | 是否使用當前環境變量 |
---|---|---|---|
execl | 參數列表 | 否 | 是 |
execlp | 參數列表 | 是 | 是 |
execle | 參數列表 | 否 | 是 |
execv | 參數數組 | 否 | 是 |
execvp | 參數數組 | 是 | 是 |
execve | 參數數組 | 否 | 否 |
?
7. 測試代碼?
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main()
{for (int i = 0; i < 8; ++i)printf(" parent i = %d\n", i);pid_t pid = fork();if (pid == 0){execlp("ps", "ps", "aux", NULL);perror("execlp");exit(1);}for (int i = 0; i < 3; ++i)printf("----------- i = %d\n", i);return 0;
}
?
參考資料?
1.?操作系統重點知識匯總