進程控制
- 進程創建
- fork函數初識
- fork函數返回值
- 寫時拷貝
- fork常規用法
- fork調用失敗的原因
- 進程終止
- 進程退出碼
- 進程常見退出方法
- 進程等待
- 進程等待必要性
- 獲取子進程status
- 進程等待的方法
- 阻塞等待與非阻塞等待
- 阻塞等待
- 非阻塞等待
- 進程替換
- 替換原理
- 替換函數
- 函數解釋
- 命名理解
- 做一個簡易的shell
進程創建
fork函數初識
在linux中fork函數時非常重要的函數,它從已存在進程中創建一個新進程。新進程為子進程,而原進程為父進程。
返回值:子進程中返回0,父進程返回子進程id,出錯返回-1。
進程調用fork,當控制轉移到內核中的fork代碼后,內核做:
- 分配新的內存塊與內存數據給子進程;
- 將父進程的部分數據內容拷貝進子進程;
- 添加子進程到系統列表當中;
- fork返回,開始調度器調度。
當一個進程調用fork之后,就有兩個二進制代碼相同的進程。而且它們都運行到相同的地方。但每個進程都將可以開始它們自己的旅程:
我們會發現,fork之前,父進程是單獨執行的,fork以后父進程和子進程就分流進行執行了,但是要注意的是父子代碼共享是fork以后共享,并不是程序所有的都共享,而且fork以后誰先執行是由調度器決定的。
fork函數返回值
那么為什么要給父進程返回子進程PID呢?
一個子進程只能擁有一個父進程,但是一個父進程可以擁有多個子進程,父進程創建子進程是為了給子進程指派任務,返回子進程的PID就可以很好的對諸多子進程進行管理。
為什么fork以后就會有兩個返回值呢?
父進程在調用fork函數以后,fork函數就會進行一系列操作,創建子進程PCB,創建子進程虛擬地址空間,創建頁表…,也就是說,在return之前,子進程就已經創建完成了,return就需要父進程子進程都執行,而return的本質就是對id的寫入,父進程返回一個id,子進程返回一個id,對于父子進程返回的id程序都需要進行執行,所以此時就會有兩個返回值。
寫時拷貝
父進程創建子進程,并不會對所有代碼和數據都進行拷貝,因為有些東西子進程只需要進行讀取,并不需要修改,與父進程共享即可,我們只有在需要修改的時候,對數據進行拷貝即可,這種延時拷貝策略,極大的提升了效率。
fork常規用法
- 一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求。
- 一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。
fork調用失敗的原因
- 系統中有太多的進程。
- 實際用戶的進程數超過了限制。
進程終止
進程退出場景:
- 代碼運行完畢,結果正確。
- 代碼運行完畢,結果不正確。
- 代碼異常終止。
進程退出碼
我們平時寫代碼過程中,一直是return 0,這是為什么呢?main函數也是個函數,系統要調用他,就需要有返回值,而return 0就表示代碼執行成功,結果正確,我們一般用非0表示結果不正確,原因在于成功了就成功了,只有一種可能,但是失敗確有多種原因。
我們可以使用echo $?命令查看最近一次進程退出的退出碼信息:
C語言當中的strerror函數可以通過錯誤碼,獲取該錯誤碼在C語言當中對應的錯誤信息:
實際上我們Linux中各種指令也是可執行程序,我們也可以看見相應的退出碼:
進程常見退出方法
正常退出
- return退出
在main函數中使用return終止是我們最常見的方式。
- 調用exit
(1)執行用戶通過 at;
(2) 關閉所有打開的;
(3) 調用_exit;
我們要注意,return只能在main函數中退出,在其他位置都是返回值,而exit可以再任意位置退出,包括調用的函數內部。
3. 調用_exit
我們會發現exit與_exit的區別就是_exit會直接終止進程,不做任何后續處理,而exit會刷新緩沖區。
我們需要知道的是,保存數據的緩沖器并不是操作系統再給我們維護,因為_exit之后,并沒有刷新緩沖區,而是C標準庫給我們維護的。
異常退出
ctrl + c,信號終止
在進程運行過程中向進程發生kill -9信號使得進程異常退出,或是使用Ctrl+C使得進程異常退出等。
進程等待
進程等待必要性
- 子進程退出,父進程如果不管不顧,就可能造成‘僵尸進程’的問題,進而造成內存泄漏。
- 進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的kill -9 也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。
- 父進程派給子進程的任務完成的如何,我們需要知道。子進程運行完成,結果對還是不對, 或者是否正常退出。
- 父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息。
獲取子進程status
- wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。
- 如果傳遞NULL,表示不關心子進程的退出狀態信息。 否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。
- status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位)
在status的低16比特位當中,高8位表示進程的退出狀態,即退出碼。進程若是被信號所殺,則低7位表示終止信號,而第8位比特位是core dump標志。
進程退出碼:(status >> 8) & 0xFF
進程退出信號:status & 0x7F
進程等待的方法
1. wait方法
pid_t wait(int*status);
//返回值:成功返回被等待進程pid,失敗返回-1。
//參數:輸出型參數,獲取子進程退出狀態,不關心則可以設置成為NULL
此時程序處于僵尸狀態,父進程并沒有對子進程進行回收,當我們使用wait以后:
1 #include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 #include<sys/wait.h>5 #include<sys/types.h>6 7 int main()8 {9 pid_t id = fork();10 if(id == -1)11 {12 perror("fork()");13 return 1;14 }15 else if(id == 0)16 {17 int cnt = 5;18 while(cnt)19 {20 printf("I am chlid: cnt:%d, pid:%d, ppid:%d\n", cnt, getpid(), getppid());21 sleep(1);22 cnt--;23 }24 exit(1);25 }26 else27 {28 pid_t ret = wait(NULL); 29 if(ret > 0)30 {31 printf("wait child sucess: ret:%d\n", ret);32 }33 while(1)34 {35 printf("I am father: pid:%d, ppid:%d\n", getpid(), getppid());36 sleep(1);37 }38 }39 return 0;40 }
父進程一直在等待當子進程運行完成,子進程運行以后,父進程對子進程進行了回收,此時程序中就只剩下父進程,子進程已經成功被回收了。
2. waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
當正常返回的時候waitpid返回收集到的子進程的進程ID;
如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
參數:
pid:
Pid= -1,等待任一個子進程。與wait等效。
Pid>0.等待其進程ID與pid相等的子進程。
status:
WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真。(查看進程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼。(查看進程的退出碼)
options:
WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子進程的ID。
創建子進程后,父進程可使用waitpid函數一直等待子進程(此時將waitpid的第三個參數設置為0),直到子進程退出后讀取子進程的退出信息。
1 #include<stdio.h>2 #include<unistd.h>3 #include<sys/wait.h>4 #include<stdlib.h>5 int main()6 {7 pid_t id = fork();8 if(id < 0)9 {10 perror("fork()");11 }12 else if(id == 0)13 {14 int cnt = 5;15 while(cnt)16 {17 printf("I am child: pid:%d, ppid:%d\n", getpid(), getppid());18 sleep(1);19 cnt--;20 }21 exit(1);22 }23 else24 {25 int status = 0;26 pid_t result = waitpid(id, &status, 0);27 if(result > 0)28 {29 printf("wait child sucess: result:%d\n", result); 30 if(WIFEXITED(status))31 {32 printf("子進程退出碼:%d\n",WEXITSTATUS(status));33 }34 else35 {36 printf("子進程收到的退出信號:%d\n", status & 0x7F);37 }38 }39 }40 return 0;41 }
當子進程正常退出時,父進程等待子進程成功:
我們可以嘗試使用kill -9命令將子進程殺死,這時父進程也能等待子進程成功,但是子進程屬于異常退出。
注意: 被信號殺死而退出的進程,其退出碼將沒有意義。
阻塞等待與非阻塞等待
阻塞等待
上述例子中,我們可以發現,當子進程未退出時,父進程一直就在等待子進程,其他什么事都沒有做,此時進程父進程就會進入阻塞狀態,當子進程運行完畢,父進程立馬被喚醒,接收子進程pid,此時狀態就叫做阻塞狀態。
非阻塞等待
父進程調用waitpid函數來進行等待,如果子進程沒有退出,waitpid這個系統調用立馬返回,父進程可以干其他事情,而且父進程會不間斷的調用waitpid函數來獲取子進程的運行情況,一旦子進程運行結束,父進程就立馬接收到,這就叫做非阻塞等待。
做法很簡單,向waitpid函數的第三個參數potions傳入WNOHANG,這樣一來,等待的子進程若是沒有結束,那么waitpid函數將直接返回0,不予以等待。而等待的子進程若是正常結束,則返回該子進程的pid。下面以一段偽代碼展示一下:
1 #include<iostream>2 #include<vector>3 #include<stdio.h>4 #include<unistd.h>5 #include<sys/wait.h>6 #include<stdlib.h>7 8 typedef void (*hander_t)();9 std::vector<hander_t> handers;10 void fun_one()11 {12 printf("這是一個臨時任務1\n");13 }14 void fun_two()15 {16 printf("這是一個臨時任務2\n");17 }18 void Load()19 {20 handers.push_back(fun_one);21 handers.push_back(fun_two);22 }23 int main()24 {25 pid_t id = fork();26 if(id == 0)27 {28 int cnt = 5;29 while(cnt)30 {31 printf("I am child: %d\n", cnt);32 sleep(1);33 cnt--;34 }35 exit(1);36 }37 else38 {39 int quit = 0;40 while(!quit)41 {42 int status = 0;43 pid_t ret = waitpid(-1, &status, WNOHANG);44 if(ret > 0)45 {46 printf("wait child sucess: exit code:%d\n", WIFEXITED(status));47 break;48 } 49 else if(ret == 0)50 {51 printf("The child process is still running, the parent processcan handle other things!!\n");52 if(handers.empty())53 {54 Load();55 }56 for(auto e : handers)57 {58 e();59 }60 }61 else62 {63 printf("wait failed\n");64 break;65 }66 sleep(1);67 }68 }69 }
此刻在運行程序我們可以發現,在等待期間,父進程也可以處理其他事情:
進程替換
替換原理
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。
當程序替換之后,有沒有創建新的進程?
程序替換只是將物理內存空間的代碼以及數據進行了替換,程序的PCB,虛擬地址空間以及頁表并沒發生改變,只是改變當前頁表的映射關系,所以并沒有創建新進程。
子進程進行替換后,會影響父進程的代碼和數據嗎?
子進程剛被創建時,與父進程共享代碼和數據,此時子進程需要被替換,就需要將父子進程共享的代碼和數據進行寫時拷貝,父子進程的代碼和數據就發生了分離,所以對子進程替換是不會影響父進程的代碼和數據的。
替換函數
其實有六種以exec開頭的函數,統稱exec函數:
1.int execl(const char* path, const char* arg, ...);
第一個參數為需要執行程序的路徑,第二個參數為可變參數列表,表示你要如何執行這個程序,并以NULL結尾。
調用execl函數以后,當前進程的所有數據和代碼都會被進行替換,包括已經執行的和未執行的,上述程序中printf已經被執行完畢,打印出來了,所以會顯示出來,本質上其實他已經被替換了。
2. int execv(const char* path, char* const argv[]);
第一個參數表示可執行程序的路徑,第二個參數是一個指針數組,存放的是你要如何執行這個可執行程序,數組以NULL結尾。
3. int execlp(const char* file, const char* arg, ...);
第一個參數是要執行程序的名字,第二個參數是可變參數列表,表示你要如何執行這個程序,并以NULL結尾。
4. int execvp(const char* file, const char* const argv[]);
第一個參數表示可執行程序的路徑,第二個參數是一個指針數組,存放的是你要如何執行這個可執行程序,數組以NULL結尾。
5. int execle(const char *path, const char *arg, ...,char *const envp[]);
第一個參數是要執行程序的路徑,第二個參數是可變參數列表,表示你要如何執行這個程序,并以NULL結尾,第三個參數是你自己設置的環境變量。
6. int execve(const char *path, char *const argv[], char *const envp[]);
第一個參數是要執行程序的路徑,第二個參數是一個指針數組,數組當中的內容表示你要如何執行這個程序,數組以NULL結尾,第三個參數是你自己設置的環境變量。
下面我們可以看見子進程進行替換是的狀態:
函數解釋
- 這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回。
- 如果調用出錯則返回-1
- 所以exec函數只有出錯的返回值而沒有成功的返回值。
命名理解
- l(list) : 表示參數采用列表 ;
- v(vector) : 參數用數組 ;
- p(path) : 有p自動搜索環境變量 PATH;
- e(env) 表示自己維護環境變量;
函數名 | 參數格式 | 是否帶路徑 | 是否使用當前環境變量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,必須自己組裝環境變量 |
execv | 數組 | 不是 | 是 |
execvp | 數組 | 是 | 不是,必須自己組裝環境變量 |
execve | 數組 | 不是 | 是 |
事實上,只有execve是真正的系統調用,其它五個函數最終都調用 execve,所以execve在man手冊 第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示:
做一個簡易的shell
shell建立一個新的進程,然后在那個進程中運行ls程序并等待那個進程結
束,然后shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序 并等待這個進程結束。
所以要寫一個shell,需要循環以下過程:
- 獲取命令行
- 解析命令行
- 建立一個子進程(fork)
- 替換子進程(execvp)
- 父進程等待子進程退出(wait)
1 #include<stdio.h>2 #include<stdlib.h>3 #include<string.h>4 #include<unistd.h>5 #include<sys/wait.h>6 7 #define NUM 10248 #define SIZE 329 #define SEP " "10 //保存完整的字符11 char cmd_line[NUM];12 //保存打散之后的字符串13 char* g_argv[SIZE];14 15 int main()16 {17 while(1)18 {19 //1.打印出提示信息:"[root@localhost myshell]# "20 printf("[root@localhost myshell]# ");21 fflush(stdout);22 memset(cmd_line, '\0', sizeof cmd_line);23 //2.獲取用戶輸入的各種鍵盤指令:"ls -a -l -i"24 if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)25 {26 continue;27 }28 cmd_line[strlen(cmd_line) - 1] = '\0';29 //3.命令行字符串解析:"la -a -l -i" -> "ls" "-a" "-l" "-i"30 g_argv[0] = strtok(cmd_line, SEP);31 int index = 1;32 if(strcmp(g_argv[0], "ls") == 0)33 {
W> 34 g_argv[index++] = "--color=auto";35 }36 if(strcmp(g_argv[0], "ll") == 0)37 {
W> 38 g_argv[0] = "ls";
W> 39 g_argv[index++] = "-l";
W> 40 g_argv[index++] = "--color=auto";41 }
W> 42 while(g_argv[index++] = strtok(NULL, SEP));//第二次解析原始字符串,出入NULL43 //4.讓父進程自己執行命令44 if(strcmp(g_argv[0], "cd") == 0)\45 {46 if(g_argv[1] != NULL)47 {48 chdir(g_argv[1]);49 }50 continue;51 }52 //5. fork()53 pid_t id = fork();54 if(id < 0)55 {56 perror("fork()");57 return 1;58 }59 else if(id == 0)60 {61 printf("下面程序是由子進程運行的\n");62 execvp(g_argv[0], g_argv);63 exit(1);64 } 65 else66 {67 int status = 0;68 pid_t ret = waitpid(-1, &status, 0);69 if(ret > 0)70 {71 printf("wait sucesss: exit code:%d\n", WEXITSTATUS(status));72 }73 }74 75 }76 return 0;77 }
結果演示: