目錄
一,進程創建,fork/vfork
1,fork創建子進程,操作系統都做了什么
2,寫時拷貝的做了什么
二,進程終止,echo $?
1,進程終止時,操作系統做了什么
2,進程終止的常見方式
3,如何正確終止一個程序
三,進程等待
1,為什么要進行,進程等待
2,如何等待,等待是什么
(1)進程等待必要性
(2)進程等待的方法
3,獲取子進程status
四,進程替換
1,替換原理
2,替換函數
3,函數理解
4,命名理解
五,微型shell,重新認識shell運行原理
1,原理
2,實現微型shell
點個贊吧!!!666
一,進程創建,fork/vfork
1,fork創建子進程,操作系統都做了什么
fork創建子進程,是不是系統里多了一個進程?是的!
進程=內核數據結構+ 進程代碼和數據!
進程代碼和數據,一般從磁盤中來,也就是你的C/C++程序,加載之后的結果!
創建子進程,給子進程分配對應的內核結構,必須子進程自己獨有了,因為進程具有獨立性!
理論上,子進程也要有自己的代碼和數據!
可是一般而言,我們沒有加載的過程,也就是說,子進程沒有自己的代碼和數據!
所以,子進程只能”使用“父進程的代碼和數據!
代碼:都是不可被寫的,只能讀取,所以父子共享,沒有問題!
數據:可能被修改的,所以,必須分離!
對于數據而言,什么時候分離?
如果,創建進程的時候,就直接拷貝分離。這楊樣會導致,可能拷貝子進程根本不會用到數據空間,即使用到了,也可能只是讀取。
而即使是OS,也不知道哪些空間可能會被寫入,即使提前拷貝了,也不會立馬使用。所以,OS選擇了寫時拷貝技術,將父子進程的數據進行分離。
OS為何要選擇寫時拷貝的技術,對父子進程進行分離?
用的時候再給你分配,是高效使用內存的一種表現,而且OS無法在代碼執行前預知哪些空間會被訪問。
所以,fork創建父子進程之后,代碼是共享的,內核的數據會各進程寫時拷貝一份。
2,寫時拷貝的做了什么
進程調用fork,當控制轉移到內核中的fork代碼后,內核做了以下操作:
分配新的內存塊和內核數據結構給子進程。
將父進程部分數據結構內容拷貝至子進程。
添加子進程到系統進程列表當中。
fork返回,開始調度器調度。
fork之后,父子進程代碼共享是所有代碼都共享的。
(1)我們的代碼匯編之后會,會有很多行代碼,而且每行代碼加載到內存之后,都有對應的地址。
(2)因為進程隨時可能被中斷(可能并沒有執行完),下次回來,還必須從之前的位置繼續執行(不是最開始的位置),這就要求CPU必須隨時記錄下,當前進程執行的位置,所以,CPU內有對應的寄存器EIP(PC程序計數器),用來記錄當前執行位置。
(3)寄存器在CPU內,只有一份,寄存器內的數據,是可以有多份的。進程的上下文數據,在fork創建子進程之后,對于子進程已經不重要了。雖然父子進程各自調度,各自都會修改EIP,但是已經不重要了,因為子進程已經認為自己的EIP起始值,就是fork之后的代碼。
二,進程終止,echo $?
1,進程終止時,操作系統做了什么
當然是要釋放進程申請的,相關內核數據結構和對應的數據與代碼,本質就是釋放系統資源。
2,進程終止的常見方式
(1)進程退出場景:
代碼運行完畢,結果正確。
代碼運行完畢,結果不正確。
代碼異常終止,程序崩潰了。
(2)進程退出碼:
查看退出碼使用:echo $?
0,表示成功。
非0,表示失敗,具體是幾,要看退出的原因。
程序崩潰的時候,退出碼無意義。一般而言,退出碼對應的return語句,沒有被執行。
[user@iZwz9eoohx59fs5a6ampomZ linux-52]$ cat exitcode.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{int number;for(number = 0; number < 100; number++){printf("%d: %s\n", number, strerror(number));}
}
[user@iZwz9eoohx59fs5a6ampomZ linux-52]$ ./exitcode
0: Success
1: Operation not permitted
2: No such file or directory
3: No such process
4: Interrupted system call
5: Input/output error
6: No such device or address
7: Argument list too long
8: Exec format error
9: Bad file descriptor
10: No child processes
。。。。。。
3,如何正確終止一個程序
使用 exit vs return 語句。
return語句,就是終止進程的,return+退出碼。
exit語句,在代碼的任何地方調用,都表示直接終止進程。
exit的頭文件是stdlib.h,exit(int status)有一個參數,這個參數就是退出碼。
exit最后也會調用exit, 但在調用exit之前,還做了其他工作:
1. 執行用戶通過 atexit或on_exit定義的清理函數
2. 關閉所有打開的流,所有的緩存數據均被寫入
3. 調用_exit
三,進程等待
1,為什么要進行,進程等待
(1)子進程退出,父進程不管子進程,子進程就要處于僵尸狀態。
(2)父進程創建子進程,是要讓子進程辦事的,那么子進程把任務完成的怎么樣,父進程關系嗎?如果需要,如果得知?如果不需要,如何處理?
2,如何等待,等待是什么
(1)進程等待必要性
之前講過,子進程退出,父進程如果不管不顧,就可能造成‘僵尸進程’的問題,進而造成內存泄漏。
另外,進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的kill -9 也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。
最后,父進程派給子進程的任務完成的如何,我們需要知道。如果子進程運行完成,結果對還是不對,或者是否正常退出。
父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息。
(2)進程等待的方法
wait方法
頭文件:
#include<sys/types.h>
#include<sys/wait.h>
函數:
pid_t wait(int*status);
返回值:
成功返回被等待進程pid,失敗返回-1。
參數:
輸出型參數,獲取子進程退出狀態,不關心則可以設置成為NULL
基本驗證-等待僵尸進程
// 會話·1[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id < 0){perror("fork");exit(1); //標識進程運行完畢,結果不正確}else if(id == 0){//子進程int cnt = 5;while(cnt){printf("cnt: %d, 我是子進程, pid: %d, ppid: %d\n", cnt, getpid(), getppid());sleep(1);cnt--;}exit(0);}else{//父進程printf("我是父進程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(7);pid_t ret = wait(NULL);//阻塞式等待if(ret > 0){printf("等待子進程成功,ret: %d\n", ret);}while(1){printf("cnt: %d, 我是父進程, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1); }}
}// 會話·2[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ ./myproc
我是父進程,pid: 31946, ppid: 30659
cnt: 5, 我是子進程, pid: 31947, ppid: 31946
cnt: 4, 我是子進程, pid: 31947, ppid: 31946
cnt: 3, 我是子進程, pid: 31947, ppid: 31946
cnt: 2, 我是子進程, pid: 31947, ppid: 31946
cnt: 1, 我是子進程, pid: 31947, ppid: 31946
等待子進程成功,ret: 31947
cnt: 31946, 我是父進程, pid: 30659, ppid: -386691177
cnt: 31946, 我是父進程, pid: 30659, ppid: -386691177
cnt: 31946, 我是父進程, pid: 30659, ppid: -386691177
^C
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。
獲取子進程退出的結果
如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,并且釋放資源,獲得子進程退出信息。
如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞。
如果不存在該子進程,則立即出錯返回。
3,獲取子進程status
wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。
如果傳遞NULL,表示不關心子進程的退出狀態信息。
否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。
status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位)
// 會話1[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ ./myproc
我是子進程: 5
我是子進程: 4
我是子進程: 3
我是子進程: 2
我是子進程: 1
子進程執行完畢,子進程的退出碼:11
[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){// 子進程int cnt = 5;while(cnt){printf("我是子進程: %d\n", cnt);sleep(1);cnt--;}exit(11);}else{// 父進程int status = 0;// 只有子進程退出的時候,父進程才會waitpid函數,進行返回,此時父進程還活著// wait/waitpid 可以在目前的情況下,讓進程退出具有一定的順序性// 將來可以讓父進程進行更多的收尾工作// id > 0 等待指定進程// id== 0 TODO// id== -1 等待任意一個子進程退出,等價于wait()pid_t result = waitpid(id, &status, 0);//阻塞狀態下,等待子進程退出if(result > 0){// 可以不這么檢測// printf("父進程等待成功,退出碼:%d\n,退出信號:%d\n", (status>>8)&0xFF, status & 0x7F);if(WIFEXITED(status)){// 子進程是正常退出的printf("子進程執行完畢,子進程的退出碼:%d\n", WEXITSTATUS(status));}else{ printf("子進程異常退出:%d\n", WIFEXITED(status));}}}
}// 會話2
[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ while :; do ps axj | head -1 && ps axj | grep myproc |grep ; sleep 1; echo "-----------------------------------------";donePPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
^C
四,進程替換
1,替換原理
fork()之后,父子各自執行父進程代碼的一部分,父子代碼共享,數據寫時拷貝各自一份。如果子進程就想有自己的代碼,執行一個全新的程序呢?這時就使用到進程替換。
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。
2,替換函數
其實有六種以exec開頭的函數,統稱exec函數:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
3,函數理解
這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回。
如果調用出錯則返回-1。
所以exec函數只有出錯的返回值而沒有成功的返回值。
最后一個參數,必須是NULL,要標識參數傳遞完畢。
案例:創建子進程,只使用最簡單的exec函數。
printf("當前進程的結束代碼!\n");為什么不打印呢?
因為,execl是程序替換,該函數成功調用之后,會將當前進程的所有代碼數據都進行替換,包括已經執行和沒有執行的,所以一旦調用成功,后續的所有代碼都不會執行。
printf("當前進程的開始代碼!\n");也被替換了,只是因為它在execl之前就打印了,才會顯示出來"當前進程的開始代碼!"。
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>int main()
{printf("當前進程的開始代碼!\n");printf("當前進程的結束代碼!\n");return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ ./myproc
當前進程的開始代碼!
當前進程的結束代碼!
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>int main()
{printf("當前進程的開始代碼!\n");execl("/usr/bin/ls", "ls", "-l", "-a", "-i", NULL);printf("當前進程的結束代碼!\n");return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ ./myproc
當前進程的開始代碼!
total 28
1449872 drwxrwxr-x 2 user user 4096 Jul 3 13:46 .
1441793 drwxrwxr-x 11 user user 4096 Jul 3 13:34 ..
1449895 -rw-rw-r-- 1 user user 64 Jul 3 13:35 makefile
1449894 -rwxrwxr-x 1 user user 8536 Jul 3 13:46 myproc
1449896 -rw-rw-r-- 1 user user 222 Jul 3 13:45 myproc.c
4,命名理解
這些函數原型看起來很容易混,但只要掌握了規律就很好記
l(list) : 表示參數采用列表
v(vector) : 參數用數組
p(path) : 有p自動搜索環境變量PATH
e(env) : 表示自己維護環境變量
exec調用舉例如下:
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 帶p的,可以使用環境變量PATH,無需寫全路徑execlp("ps", "ps", "-ef", NULL);// 帶e的,需要自己組裝環境變量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 帶p的,可以使用環境變量PATH,無需寫全路徑execvp("ps", argv);// 帶e的,需要自己組裝環境變量execve("/bin/ps", argv, envp);exit(0);
}
五,微型shell,重新認識shell運行原理
1,原理
用下圖的時間軸來表示事件的發生次序。其中時間從左向右。shell由標識為sh的方塊代表,它隨著時間的流逝從左向右移動。shell從用戶讀入字符串"ls"。shell建立一個新的進程,然后在那個進程中運行ls程序并等待那個進程結束。
然后shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序, 并等待這個進程結束。所以要寫一個shell,需要循環以下過程:
1. 獲取命令行
2. 解析命令行
3. 建立一個子進程(fork)
4. 替換子進程(execvp)
5. 父進程等待子進程退出(wait)
2,實現微型shell
根據這些思路,和我們前面的學的技術,就可以自己來實現一個shell了.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>#define NUM 1024
#define SIZE 32
#define SEP " "// 保存完整的命令行字符串
char cmd_line[NUM];
// 保存打散之后的命令行字符串
char *g_argv[SIZE];// shell 運行原理:通過子進程執行命令,父進程等待&&解析命令
int main()
{//0.命令行解釋器,一定是一個常駐內存的進程,不退出while(1){//1.打印出提示信息 [root@localhost myshell]#printf("[root@localhost myshell]# ");fflush(stdout);memset(cmd_line, '\0', sizeof cmd_line);//2.獲取用戶的輸入,輸入的是各自指令和選型"ls -a -l -i"if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){continue; }cmd_line[strlen(cmd_line)-1] = '\0';//去掉空行,把\n變成\0,字符串長度下標就是\n,"ls -a -l -i\n\0 "//printf("echo: %s\n", cmd_line);//3.把輸入的命令行字符串解析,從"ls -a -l -i",變成"ls","-a","-i","-l"//第一次調用,要傳入原始字符串g_argv[0] = strtok(cmd_line, SEP);int index = 1;// 加顏色if(strcmp(g_argv[0], "ls") == 0){g_argv[index++] = "--color=auto"; }// 設置ll命令別名if(strcmp(g_argv[0], "ll") == 0){g_argv[0] = "ls";g_argv[index++] = "-l";g_argv[index++] = "--color=auto";}//第二次調用,如果還要解析原始字符串,傳入NULLwhile(g_argv[index++] = strtok(NULL, SEP));//for(index = 0; g_argv[index]; index++)// printf("g_argv[%d]: %s\n", index, g_argv[index]);//4.執行命令,內置命令,讓父進程(shell)自己執行的命令,就叫做內置(內鍵)命令 //內置命令,本質就是shell中的一個函數調用if(strcmp(g_argv[0], "cd") == 0) {if(g_argv[1] != NULL) chdir(g_argv[1]);continue;}//5.父進程調用子進程執行,fork()//子進程pid_t id = fork();if(id == 0){printf("下面的功能讓是子進程執行的\n");execvp(g_argv[0], g_argv);exit(1);}//父進程int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));}
}