目錄
進程創建
fork函數
寫時拷貝
進程終止?
進程退出碼
exit函數
_exit函數
return,exit _exit之間的區別和聯系
進程等待
進程等待的必要性
獲取子進程status
進程等待的方法
wait
waipid
多子進程創建理解
非阻塞輪詢檢測子進程
進程程序替換
替換原理
exec系列替換函數
命名理解
shell(linux指令解釋器)簡易版
進程創建
fork函數
我喜歡叫分叉函數,用處就是,在當前進程的基礎下,創建一個子進程。
返回值
返回值有三種:0? -1? 子進程的PID
返回值 == 0:此時說明當前進程是子進程。
返回值 == -1:說明子進程創建失敗。
返回值 == 子進程的PID:說明此時是父進程(因為父進程要管理子進程的退出)。
內核中fork函數:
- 分配一塊內存創建子進程PCB,將其放入管理隊列中。
- 父進程的內容拷貝到子進程。
- 添加子進程到系統進程列表中。
- fork返回,開始執行代碼。
我們發現在fork前的代碼執行了一次,后面的代碼子進程和父進程都執行了一次。
fork函數為什么能有兩種返回值?
?fork函數在內核中,給子進程分配空間并且管理起來。此時fork函數還沒結束的。它還有一個return語句:
此時在返回時二者都要進行返回,所以這個返回值就可以有兩個。?然后我們在PCB中是有保存PID和PPID的,此時就可以根據這個確認誰是子進程誰是父進程,此時就能控制返回值。
寫時拷貝
顧名思義:只有寫的時候才進行拷貝。
我們子進程和父進程是共享一塊空間的,為什么不直接給子進程創建新的空間,如果父子進程只有讀操作,重新開辟空間copy一份父進程的變量給子進程不是浪費嗎?
所以只有子進程和父進程需要更改變量時,才需要把父進程的變量拷貝一個新的變量給到子進程。
什么時候,代碼也會被替換?那就是程序替換的范疇了下面會說。
fork通常用于程序替換
?要和 exec函數等程序替換函數配合使用。
可以把子進程的執行邏輯直接切換成別的程序。但是父進程依然可以對子進程進行管理(守護進程)。
進程終止?
進程退出三種情況:
1.代碼運行完畢,結果正確。
2.代碼運行完畢,結果不正確。
3.代碼異常終止(進程崩潰)。
進程退出碼
我們先了解下:main函數到底是什么?
main函數在我們寫程序中開來就是一切的起源。但是他是怎么被啟動的。
VS2013中main函數被一個叫做__tmainCRTStartup的函數調用。而這個函數又被操作系統通過加載器調用。這就是說main函數也是被系統操作的。
通常我們main函數的return值是0,而這個0,其實就是一個退出碼。
0退出碼代表執行成功,0以外都代表了執行錯誤
?我們知道C語言中有個函數可以獲取錯誤代碼的信息 strerror(數字)。
我們編寫一個這樣的代碼
執行后?
?我們發現只有0是success,其他都是一些錯誤表示,所以退出碼的選擇。
可以用echo $??查看上一次程序的退出碼是什么。
exit函數
exit函數就是用來用來退出程序的。并且有三步:
1.執行用戶定義的atexit清理函數。
2.關閉這個進程打開的所有流,所有的緩存數據都被寫入。
3.調用_exit函數終止進程。
所有數據都會被寫入相對的流重點,才會退出。
_exit函數
_exit函數就是 exit函數的退化版,因為exit函數已經包含了_exit。因為_exit不會做任何處理,直接關閉進程。
?這個代碼執行結果就是:沒有任何輸出,我們的printf是先寫到對應屏幕的緩沖區,此時_exit在還沒寫入時就把程序關了,所以就沒有任何輸出結果。
return,exit _exit之間的區別和聯系
return通常都只是返回,但是在main函數中返回即終止主進程,整個代碼完結。exit和_exit就是在子函數中強制退出進程用的。
下圖是exit和_exit的區別。
進程等待
進程等待的必要性
1.僵尸進程:如果父進程一直不讀取子進程退出信息,子進程就變成僵尸進程了,內存泄漏
2.僵尸進程無法被主動刪除。
3.對于進程來說,最關心自己的就是父進程,因為父進程需要知道子進程的任務完成情況。
4.父進程通過進程等待的方式,回收子進程資源,獲取對應的退出信息。
獲取子進程status
這是一個wait和waitpid中的一個參數,這個參數是輸入輸出型(傳入后,在函數中被更改后傳出)。如果這個參數給到NULL則表示不關心子進程退出情況。
status是一個整型變量,但是status其實作用就是一個類似位圖的運用。
?這里只研究低16位。后八位代表終止信號,即退出碼。前7位代表的終止信號,即信號碼,第八位的core dump標志。
?正常來說退出后只有退出狀態被設置,剩下的都沒設置,而終止后core dump也會被設置。
exitCode = (status >> 8) & 0xFF; //退出碼
exitSignal = status & 0x7F; //退出信號
通過位運算就能取出對應的碼和信號值
進程等待的方法
wait
函數原型:pid_t wait(int * status);
作用:等待任意子進程
返回值:成功則返回對應的子進程pid,失敗返回-1
參數:status,獲取狀態碼。不關心就設置為NULL
waipid
函數原型: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
WNOHANG:設置以后,本來就是子進程不退出,父進程就一直卡著不動,如果設置了子進程沒返回,那就先返回0給父進程,此時給父進程在一個循環里持續獲取子進程退出信息,再循環體里父進程就可以繼續做自己的事情
多子進程創建理解
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t ids[10];for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){//childprintf("child process created successfully...PID:%d\n", getpid());sleep(3);exit(i); //將子進程的退出碼設置為該子進程PID在數組ids中的下標}//fatherids[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 successprintf("wiat child success..PID:%d\n", ids[i]);if (WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killedprintf("killed by signal %d\n", status & 0x7F);}}}return 0;
}
這個代碼就是用來創建了十個子進程,然后把10個子進程的的pid放到數組里,之后再讓父進程for循環等待子進程結束。
?可以看到我們上面的進程創建出來好,父進程是一個個等待結束的。
那能不能讓父進程在沒子進程退出時在執行一套自己的邏輯呢?
非阻塞輪詢檢測子進程
這里就要用到之前說的WNOHANG:WAIT NO HANG,就是說等待不會被掛起的意思。
此時我們給waitpid函數加上WNOHANG。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){//childint count = 3;while (count--){printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());sleep(3);}exit(0);}//fatherwhile (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;
}
此時發現我們父進程除了等待外,還有其他的判斷語句此時就是父進程在等待時能做其他事情的意思
進程程序替換
替換原理
本來子進程和父進程執行的是同一個代碼,只是可以用if else 分支,但是怎么讓子進程直接去執行另一個程序呢?
如上圖,在我們創建好子進程后,將其與磁盤中的程序進行替換。?
進程替換時,有沒有新的進程創建
都叫進程替換了,那就說明了就是把子進程替換了而已,只是這個新的程序依然可以被父進程管理。pid,進程地址空間,頁表都沒變,只有代碼和數據變了。
那會發生寫時拷貝嗎?
當然,這里的整個 子進程的代碼數據都變了,所以和父進程就發生了寫時拷貝,此時就不會影響父進程的數據了
exec系列替換函數
總共有6種exec系列函數
一、
int execl(const char *path, const char *arg, ...);
第一個參數是程序路徑,第二個可變參數列表,表示的就是這個程序的選項,怎么執行這個程序。以NULL結尾
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
?上述代碼就是:ls程序的執行。
二、
int execlp(const char *file, const char *arg, ...);
第一個參數是要執行程序的名字,和環境變量搭配使用。?第二個和上面一樣代表怎么執行這個程序
execlp("ls", "ls", "-a", "-i", "-l", NULL);
三、
int execle(const char *path, const char *arg, ..., char *const envp[]);
?第一個參數是地址,第二個參數還是老樣子,第三個參數是自己想設置的環境變量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
如上代碼:執行后,在環境變量中就多了一項 MYVAL,此時這個VAL就能被直接使用。在一些程序中,可以配置一些環境變量,便于使用。
四、
int execv(const char *path, char *const argv[]);
?第一個參數是要執行代碼的地址,第二個就是一個字符指針(字符串)數組,只是把上面的在參數里傳遞,改編成了傳遞一個字符串數組。(一樣要以NULL結束)
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
五、
int execvp(const char *file, char *const argv[]);
?第一個參數是要執行的程序的名字,第二個參數還是執行程序的選項的字符串數組。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
六、
int execve(const char *path, char *const argv[], char *const envp[]);
?第一個參數是路徑,第二個參數是選項和執行內容,第三個參數是要設置的環境變量。
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);
上述全部函數成功調用,則直接執行新的程序,不再返回。
失敗則返回-1.
命名理解
我們發現exec系列的函數,開頭都是exec只是后面帶的剩余字母不同:
l(list):表示參數采用列表的形式,可變參數列表。
v(vector):采用數組的形式。
p(path):表示自動搜索環境變量PATH,進行相應程序的查找。
e(env):表示可以傳入自己設置的環境變量。
只有execve是系統調用,其它5個函數最終都是調用execve。?
shell(linux指令解釋器)簡易版
這里制作的shell采用父子進程制作,即bash(shell)接收命令,然后創建子進程然后把子進程的邏輯替換為對應指令即可
#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#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){//childexecvp(myargv[0], myargv); //child進行程序替換exit(1); //替換失敗的退出碼設置為1}//shellint status = 0;pid_t ret = waitpid(id, &status, 0); //shell等待child退出if (ret > 0){printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出碼}}return 0;
}