文章目錄
- 一、fork的補充
- 1.1.寫時拷貝
- 1.2.fork呼叫失敗的原因
- 二、行程終止
- 2.1.退出碼
- 2.2.正常退出
- return
- exit
- _exit和exit的區別
- 2.3.例外退出
- 三、行程等待
- 3.1.行程等待的方法
- wait
- waitpid
- status
- 3.2.創建多行程
- 3.3.非阻塞等待子行程
- 3.4.總結
- 四、行程程式替換
- 4.1.行程替換的函式
- 4.2. execve
- 五、實作一個簡單的shell
- 六、補充和總結內容
一、fork的補充
在之前已經了解了fork函式,這個函式是以父行程為“模板”創建子行程,父子行程的所有代碼共享,這是因為代碼是不可被修改的,所以各自私有代碼的話會浪費空間,
其回傳值為:
子行程中回傳0,父行程中回傳子行程的PID,子行程創建失敗回傳-1,因為一個父行程可以創建多個子行程,而一個子行程只能有一個父行程,因此,對于子行程來說,父行程是不需要被標識的;而對于父行程來說,子行程是需要被標識的,因為父行程創建子行程的目的是讓其執行任務的,父行程只有知道了子行程的PID才能很好的對該子行程指派任務,
fork的作業程序具體如下:
- 父行程初始化,
- 父行程呼叫fork創建子行程,fork為系統呼叫,因此進入內核,
- 內核根據父行程復制出一個子行程,父行程和子行程的PCB資訊相同,代碼和資料也相同,因此,子行程和父行程一樣,做完初始化,剛掉用了fork進入內核,還沒有從內核回傳,
- 現在又兩個一模一樣的行程都呼叫了fork進入內核等待從內核回傳(實際上只有父行程呼叫了fork一次),此外系統中還有很多其他行程也等待從內核回傳,是父行程先回傳還是子行程先回傳,還是這兩個行程都等待,系統調度執行了其他的行程,取決于內核的調度演算法,
- 如果某個時刻父行程被調度指向,從內核回傳后就從fork函式回傳,回傳值是子行程的PID,
- 如果某個時刻子行程被調度執行了,從內核回傳后就從fork函式回傳,回傳值是0.

fork函式的特點概括起來就是“呼叫一次,回傳兩次”,在父行程中呼叫一次,在父行程和子行程中各回傳一次,開始是一個控制流程,呼叫fork之后發生分叉,變成兩個控制流程,這也是fork(分叉)名字的由來,子行程中fork回傳值是0,父行程是子行程的PID(從根本上說fork是從內核回傳的,內核自有辦法讓父行程和子行程回傳不同的值),這樣當fork函式回傳后,可以根據回傳值的不同讓父行程和子行程執行不同的代碼,
另外,一般而言,通常要讓子行程先退出,因為父行程可以很容易對子行程進行管理(垃圾回收),而且子行程創建出來是用來處理業務的,所以需要父行程幫忙拿到子行程執行的結果,
1.1.寫時拷貝
父子代碼共享,父子再不寫入時,資料也是共享的,當任意一方試圖寫入,便以寫時拷貝的方式各自一份副本:

寫時拷貝相比于創建行程時就拷貝節約了記憶體空間,因為子行程不對資料進行寫入的情況下,沒有必要對資料進行拷貝,
寫時拷貝可以保證在多行程運行時,各行程獨享各自的資源,多行程運行期間互不干擾,不讓子行程的修改影響到父行程,實作行程獨立性,
另外,寫時拷貝并不會把全部的資料都拷貝過去,需要多少就拷貝多少,比如資料一共有10M,子行程只需要對其中的1M進行修改,作業系統只需要拷貝修改的那1M,
1.2.fork呼叫失敗的原因
- 系統中有太多的行程
- 實際用戶的行程數超過限制,一個用戶創建的行程數量是有限的,
二、行程終止
行程退出只有三種情況:
-
代碼運行完畢,結果正確,
-
代碼運行完畢,結果不正確,
-
代碼例外終止(行程崩潰),
2.1.退出碼
可以通過 echo $?查看最近一次行程的退出碼:
退出碼分為以下幾類:
- 從main函式中
return回傳,(正常退出)(0表示正常退出,非0表示錯誤退出) - 呼叫
exit,(正常退出) - 呼叫
_exit,(正常退出) ctrl + c,信號終止,(例外退出)

Linux中自帶的命令也是一個可執行程式,所以它們也會有行程退出碼:

這些退出碼都有含義,從而幫助用戶確認執行失敗的原因,而這些退出碼具體代表什么含義是人為規定的,不同環境下相同的退出碼的字串含義可能不同,可以使用strerror函式確認這些退出碼的含義:


2.2.正常退出
return
這種方式是最常用的退出方式,這也是為什么main函式最后要寫一個return 0的原因,因為0表示正常退出,
exit

exit和return是有差別的,exit是退出整個行程,在行程的任何地方都可以呼叫從而退出整個行程,而return是終止當前函式,并不會將行程終止,在main函式中呼叫的return則會使行程退出,
執行return num等同于執行exit(num),因為呼叫main函式運行結束后,會將main函式的回傳值當做exit的引數來呼叫exit函式,
exit的引數就是一個行程的退出碼:


_exit和exit的區別
exit()函式定義在stdlib.h中,而_exit()定義在unistd.h中,exit()和_exit()都用于正常終止一個函式,但_exit()直接是一個sys_exit系統呼叫,而exit()則通常是普通函式庫中的一個函式,它會先執行一些清除操作,例如呼叫執行各終止處理函式、關閉所有標準IO等,然后呼叫sys_exit:

exit的退出:


_exit退出:


2.3.例外退出
例外退出的情況一般有下面兩種:
-
向行程發生信號導致行程例外退出,
在行程運行程序中向行程發生kill -9信號使得行程例外退出,或是使用Ctrl+c使得行程例外退出等, -
代碼錯誤導致行程運行時例外退出,
比如代碼指標越界導致行程例外退出,或是出現除0的情況使得行程運行時例外退出等,
三、行程等待
由于需要保證子行程先退出(不這么做會造成僵尸行程,使記憶體泄漏),所以父行程需要通過行程等待的方式,回收子行程資源,獲取子行程的退出資訊,
3.1.行程等待的方法
wait

wait()等待任一僵死的子行程,將子行程的退出狀態(退出值、回傳碼、回傳值)保存在引數status中,即行程一旦呼叫了wait,就立即阻塞自己,由wait分析是否當前行程的某個子行程已經退出,如果找到這樣一個已經變成僵尸的子行程,wait就會收集這個子行程的資訊,并把它徹底銷毀后回傳;如果沒有找到這樣一個子行程,wait就會一直阻塞在這里,直到有一個出現為止,如果成功,回傳該終止行程的PID,否則回傳-1,其引數為獲取子行程的退出狀態,不關心可設定為NULL,
使用下面的程式驗證:

使用以下監控腳本對行程進行實時監控:
while :; do ps axj | head -1 && ps axj | grep test |
grep -v grep;echo "######################";sleep 1;done

可以看到子行程并沒有變成僵尸行程,而是被父行程清理掉了,另外父行程在運行到wait(NULL)一句的時候會阻塞等待,直到清理完子行程才往下執行,
如果把wait(NULL)一句去掉:


可以看到子行程在退出后會變成僵尸行程,
waitpid

相比于wait,waitpid等待識別符號為pid的子行程退出,將該子行程的退出狀態(退出值、回傳碼、回傳值)保存在引數status中,
其三個引數:
- pid:待等待子行程的pid,若設定為-1,則等待任意子行程,
- status:獲取子行程的退出狀態,不關心可設定為NULL,
- options:規定呼叫的行為,當這個引數設定為
WNOHANG表示如果沒有子行程退出,則立即回傳0,不等待子行程退出;設定為WUNTRACED表示回傳一個已經停止但尚未退出的子行程的資訊,
status
status是一個輸出型引數,也就是會一個整形變數的地址傳進去,子行程退出,作業系統會從行程PCB中讀取資訊保存在status指向的變數中,將子行程的退出資訊反饋給父行程,如果傳遞NULL,表示不關心子行程的退出狀態資訊,
status不能簡單的當作整形來看待,可以當作位圖來看待,在status的低16位元位當中,高8位表示行程的退出狀態,即退出碼,行程若是被信號所殺,則低7位表示終止信號,而第8位位元位是core dump標志,

因此如果想檢測行程是否被信號所殺,只需要檢測第七位是否為0即可,如果為0則為正常終止,
以下面的程式為例:


可以看到st之所以是256,是因為正常終止時前八位全是0,后八位才是退出碼,所以如果相獲取退出碼的話需要把st右移八位然后按位與上1111 1111即可,

同時由于只有后八位才是退出碼,因此退出碼不能超過255,否則會因為越界而無法存盤,比如:




如果是被信號所殺且要拿到退出信號,只需要按位與上0x7F即可:


如果這個值為0,就說明沒有收到任何信號:

如果子行程中存在錯誤:



SIGFPE是除零例外信號,
因此可以通過status這個引數判斷子行程是否運行正確,并且判斷其運行成功后的退出碼:

當然上面這些如果自己來寫的話就太麻煩了,所以系統當中提供了兩個宏來獲取退出碼和退出信號:
- WIFEXITED(status):用于查看行程是否是正常退出,本質是檢查是否收到信號,如果正常終止子行程則為真,相當于
!(status&0x7F) - WEXITSTATUS(status):如果WIFEXITED(status)非零,則說明正常終止子行程,此時這個宏用于獲取行程的退出碼,相當于
(status>>8)&0xFF
因此上面的程式可以改成這樣:



3.2.創建多行程
我們還可以同時創建多個子行程,然后讓父行程依次等待子行程退出:
#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){
//child
printf("child process created successfully...PID:%d\n", getpid());
sleep(1);
//子行程要執行的代碼
//... ...
exit(i); //將子行程的退出碼設定為該子行程PID在陣列ids中的下標
}
//father
ids[i] = id;
}
for (int i = 0; i < 10; i++){
int st = 0;
pid_t ret = waitpid(ids[i], &st, 0);
if (ret >= 0){
printf("wiat child success..PID:%d\n", ids[i]);
if (WIFEXITED(st)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(st));
}
else{
//signal killed
printf("killed by signal %d\n", st & 0x7F);
}
}
}
return 0;
}

3.3.非阻塞等待子行程
前面提到過,options的引數設定為WNOHANG表示如果沒有子行程退出,則立即回傳0,不等待子行程退出,所以可以使用這個引數讓父行程不等子行程而是做別的事情:
#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){
//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 st = 0;
pid_t ret = waitpid(id, &st, WNOHANG);
if (ret > 0){
printf("wait child success!\n");
printf("exit code:%d\n", WEXITSTATUS(st));
break;
}
else if (ret == 0){
printf("child is not quit,check later!\n");
sleep(1);
}
else{
printf("child exit error!\n");
break;
}
}
return 0;
}

雖然阻塞式等待在等待的時候不能干別的事情,但是計算機中大部分等待方式都是阻塞式等待,因為阻塞式等待更簡單,
3.4.總結
什么是行程等待:是父行程通過wait等待系統呼叫,用來等待子行程狀態的一種現象,
為什么要行程等待:1.防止子行程發生僵尸問題,進而產生記憶體泄漏 2.讀取子行程的行程狀態
四、行程程式替換
父子行程之間代碼是共享的,所以實際上父子行程執行的是同一個程式,若想讓子行程執行另一個和子行程不同的程式,往往需要呼叫exec函式,

程式替換并是創建一個新的行程,因為PCB沒有被重新創建,PID也沒有重新生成,
4.1.行程替換的函式
行程替換的函式一共有六個,統稱exec函式,都在頭檔案<unistd.h>中:

這六個函式的第一個引數代表的是替換的目標程式路徑(路徑或者程式名字),
第二個引數和后面的…代表如何執行目標程式,在命令列中怎么呼叫執行,就怎么傳遞,
exec系列函式如果函式回傳了,或者執行了后續的代碼,那一定是程式替換錯了,因為函式如果呼叫成功,則加載指定的程式并從啟動代碼開始執行,不再回傳,如果呼叫出錯,回傳-1,
這六個函式的名字是由exec加其他字母組成,每個字母表示其引數的含義:
- l(list) : 表示引數采用串列,其引數是可變引數串列,可以傳多個引數,并以NULL結尾,
- v(vector) : 引數用陣列,引數要寫到陣列里,然后傳入一個陣列,
- p(path) : 有p在執行的時候會自動搜索環境變數PATH,帶p的第一個引數是file,不帶p則是path,因為如果要進行程式替換,必須要先找到要替換的程式,以
ls為例,帶p的就可以不用傳路徑而是只傳名字就行,因為會自動搜索環境變數,不帶p的則必須傳路徑, - e(env) : 表示自己維護環境變數
| 函式名 | 引數格式 | 是否帶路徑 | 是否使用當前環境變數 |
|---|---|---|---|
| execl | 串列 | 否 | 是 |
| execlp | 串列 | 是 | 是 |
| execle | 串列 | 否 | 否,需自己組裝環境變數 |
| execv | 陣列 | 否 | 是 |
| execvp | 陣列 | 是 | 是 |
| execvpe | 數字 | 是 | 否,需自己組裝環境變數 |
| execve | 陣列 | 否 | 否,需自己組裝環境變數 |
int execl(const char *path, const char *arg, ...);
由于不帶p不能自動搜索環境變數,因此第一個引數是要執行程式的路徑,第二個引數是可變引數串列,表示如何執行這個程式,并以NULL結尾,
以目標程式是ls -a -l -i為例:

注意這里的"/usr/bin/ls代表的是找到這條命令,后面的"ls","-a","-l"才是執行,所以后面不能省略ls

一旦替換成功,接下來的行程就會執行被替換的程式,原來程式后面的代碼由于已經被替換,就不會執行了,
int execlp(const char *file, const char *arg, ...);
帶上p之后第一個引數就不需要寫全路徑了,因為會自動搜索環境變數,當然如果環境變數中沒有,還是要帶上路徑的:

int execv(const char *path, char *const argv[]);
第一個引數是全路徑,第二個引數是一個陣列,不再是可變引數串列:


int execvp(const char *file, char *const argv[]);
帶上p之后第一個引數就不需要寫全路徑了,因為會自動搜索環境變數,當然如果環境變數中沒有,還是要帶上路徑的:

int execle(const char *path, const char *arg, ...,char *const envp[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
exec系列函式能呼叫系統程式,也可以呼叫自己寫的程式,所以可以在自己寫的程式中呼叫自己定義的環境變數,execle的第三個引數的作用就是傳入一個自己定義的環境變數,比如讓myexe程式呼叫test程式,然后在test輸出自己定義的環境變數MYENV:



4.2. execve

上面這些函式都是基于execve函式做的封裝,只有execve函式才是真正的系統呼叫:

之所以設計這么多的exec函式主要是為了滿足不同的場景需求,
五、實作一個簡單的shell
shell需要執行的邏輯非常簡單,其只需回圈執行以下步驟:
- 獲取命令列,
- 決議命令列,
- 創建子行程,(fork)
- 替換子行程讓子行程執行指令,(execvp)
- 等待子行程退出,(wait)

之所以要創建子行程是因為如果要執行的命令錯誤,子行程掛掉并不影響父行程,
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#define SIZE 256
#define NUM 16 //命令列引數的個數
int main()
{
char cmd[SIZE];
const char* cmd_line="[hjl@VM-0-16-centos ~]$ ";
while(1)
{
cmd[0]=0;
printf("%s",cmd_line);
fgets(cmd,SIZE,stdin);
cmd[strlen(cmd)-1]='\0';//將最后的'\n'替換為'\0'
//將命令字串分割
char*args[NUM];
args[0]=strtok(cmd," ");
int i=1;
do
{
args[i]=strtok(NULL," ");
if(args[i]==NULL)
{
break;
}
i++;
}while(1);
//創建子行程讓其執行命令字串
pid_t id=fork();
if(id<0)
{
perror("fork error!\n");
continue;
}
if(id==0)//子行程
{
execvp(args[0],args);//替換子行程使用exec系列函式
exit(1);
}
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("status code:%d\n",(status>>8)&0xFF);
}
}
return 0;
}

六、補充和總結內容
行程創建的兩種方式:1.運行一個可執行程式(由bash創建)2.fork創建(由我們自己創建)
行程創建出來,作業系統除了將行程的二進制代碼和資料加載到記憶體之外,為了便于管理還要給行程創建對應的資料結構(PCB、地址空間、頁表等)
父子行程相互之間是獨立的,不會相互影響,資料各自私有,采用寫時拷貝
在行程的任何一個地方呼叫exit()都會終止行程,return只會終止當前函式,exit()和_exit()的區別在于exit()會做一系列清理作業(執行清理函式,沖刷緩沖區等),
終止一個行程時作業系統要回收行程的資源,代碼和資料可以優先被釋放(因為永遠也不會被訪問了),資料結構釋放的比較晚(因為要記錄退出資訊),
行程替換是將原來行程的資料結構大體不變的情況下(PCB不變,頁表的映射關系改變),將新程式的代碼和資料覆寫原來的行程,程式替換要由作業系統來完成,因為新程式存盤在磁盤上,而行程在記憶體中,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/316617.html
標籤:其他
上一篇:介面測驗工具
