1. 程式人生 > >Linux:程序控制(fork/vfork)(程序終止:exit/_exit)(程序等待:wait/waitpid/status)(程序替換:exec函式/shell實現)

Linux:程序控制(fork/vfork)(程序終止:exit/_exit)(程序等待:wait/waitpid/status)(程序替換:exec函式/shell實現)

目錄

 

程序建立

fork原理

fork函式返回值

fork用法和呼叫失敗的原因

vfork函式

總結

程序終止

程序退出場景:

程序常見退出方法

return退出

_exit函式

exit函式

總結:

程序等待

程序等待的重要性

程序等待的方法

wait方法

waitpid方法

總結

獲取子程序status

總結:

程序程式替換

替換原理

替換函式

函式解釋與命名原理

總結

簡單的shell

程式碼

圖解

總結


程序建立

fork原理

在Linux中fork函式非常重要,它從已經存在的程序中建立一個新的子程序。

新程序為子程序,原程序為父程序。(以父程序為模板來建立子程序)

(pcb,記憶體指標,程式計數器,上下文資料)從下一句程式碼開始執行

每個程序都有自己的虛擬地址空間,子程序複製的只是資料,虛擬地址空間父子程序各一份

,因此父子程序資料的是獨有的,但是程式碼共享。

#include<unistd.h>
pid_t fork(void);

//返回值:子程序中返回0,父程序返回子程序的id,出錯返回-1

程序呼叫fork,當控制轉移到核心中的fork程式碼之後,核心做:

  1. 分配新的記憶體塊和核心資料結構給子程序
  2. 將父程序部分資料結構內容拷貝至子程序
  3. 新增子程序到系統程序列表中
  4. fork返回,開始排程器排程

當一個程序呼叫fork之後,就有兩個二進位制程式碼相同的程序,而且執行到相同的地方

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<errno.h>
  4 int main()
  5 {
  6     pid_t pid;
  7     printf("before:pid : %d\n",getpid());
  8     pid = fork();
  9     if(pid == -1)
 10     {
 11         perror("fork");
 12     }
 13     printf("after:pid :%d  and fork return %d\n",getpid(),pid);
 14     sleep(1);
 15     return 0;
 16 }

  1. 有三行輸出,一行before,兩行after程序3545先列印before還有after,另一個只有after
  2. 因為子程序是從fork建立之後開始執行,而不是從main函式開始執行,所以不會列印before
  3. fork之後父子程序兩個執行流分別執行,誰先完全由排程器決定

fork函式返回值

  1. 子程序返回0
  2. 父程序返回子程序的id

為什麼不能反過來?

系統提供了getpid()和getppid()可以獲取當前程序和的、當前程序的父程序的id,但是無法獲得子程序的id,所以父程序必須返回子程序id(才能知道子程序id)

寫時拷貝:通常,父子程式碼共享,父子不在寫入時,資料也是共享的,當任意一方試圖寫入時,便以寫時拷貝的方式各自有一份副本

                (修改時更新頁表,指向新的記憶體區域)

fork用法和呼叫失敗的原因

用法

  1. 一個父程序希望複製自己,使父子程序實現不同的程式碼段(例如:父程序等待客戶端請求,生成子程序取處理請求)
  2. 一個程序要執行一個不同的程式(例如:子程序從fork返回後,呼叫exec函式)

呼叫失敗原因

  1. 系統中有太多程序
  2. 實際使用者的程序超過了限制

vfork函式

同樣用來建立子程序,但是

  1. vfork用於建立一個子程序,而子程序和父程序共享地址空間(fork的子程序有獨立的地址空間)
  2. vfork保證子程序先執行,在它呼叫exec或exit之後父程序才可能被排程執行
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<errno.h>
  4 #include<stdlib.h>
  5 int i = 100;
  6 int main()
  7 {
  8     pid_t pid = vfork();
  9     if(pid == -1)
 10     {
 11         perror("vfork");
 12         return -1;
 13     }
 14     else if(pid == 0)
 15     {
 16         i = 200;
 17         printf("child: i = %d pid:%d\n",i,getpid());
 18         exit(0);
 19     }
 20     else
 21     {
 22         printf("parent: i = %d pid:%d\n",i,getpid());
 23     }
 24 
 25     return 0;
 26 }

  1. 父子程序輸出都是200,可見子程序改變了父程序的變數值,因為子程序在父程序的地址空間中執行
  2. 子程序沒有執行其他程式或者退出之前,父程序阻塞在vfork處不返回(vfork建立子程序先執行,直到exit(0)退出,父程序接著後面程式碼執行)
  3. 如果不加exit(0),則子程序一直跑完程式碼,return返回,但是由於父子程序共享所以資源以及釋放,導致父程序死迴圈(呼叫棧以及混亂)

總結

1.  fork  ():子程序拷貝父程序的資料段,程式碼段 ,子程序有自己的虛擬地址空間,父子程序資料獨有(寫時拷貝)程式碼共享
    vfork ( ):子程序與父程序共享資料段 (共享虛擬地址空間)
2.  fork ()父子程序的執行次序不確定 
    vfork ()保證子程序先執行,在呼叫exec 或exit 之前與父程序資料是共享的,在它呼叫exec
                   或exit 之後父程序才可能被排程執行。 
3.  vfork ()保證子程序先執行,在它呼叫exec 或exit 之後父程序才可能被排程執行。如果在
                   呼叫這兩個函式之前子程序依賴於父程序的進一步動作,則會導致死鎖。
 

程序終止

程序退出場景:

  1. 程式碼執行完畢,結果正確
  2. 程式碼執行完畢,結果不正確
  3. 程式碼異常終止

程序常見退出方法

  1. 從main返回
  2. 呼叫exit
  3. _exit
  4. 異常退出(ctrl + c)

return退出

return 是一種最常見的退出方式,執行return n 相當於exit(n),因為呼叫main函式時將會呼叫main的返回值當exit的引數

_exit函式

#include<unistd.h>
void _exit(int status);
//引數:status 定義了程序的終止狀態,父程序通過wait來獲取該值

直接釋放資源(粗暴)

說明:雖然status是int,但是僅有低8位可以被父程序使用,所以_exit(-1),在終端執行時返回255

echo $?  可以獲取狀態返回碼(0--255)僅有低8位!

exit函式

#include<unistd.h>
void exit(int status);
  1. exit(逐步釋放資源)最後也會呼叫_exit,但在這之前,會做其他工作
  2. 先執行使用者通過atexit或on_exit定義的清理函式
  3. 關閉所有開啟的流,所有快取資料均被寫入
  4. 呼叫_exit

具體區別如下:逐步釋放和粗暴釋放

總結:

return:只有在main中執行還會退出程序,在main中return跟呼叫exit效果一樣

exit:在任意位置都可以退出程序,退出前會重新整理緩衝,關閉檔案,做很多操作

_exit:粗暴的退出程序,什麼也不幹,直接釋放資源

程序等待

程序等待的重要性

  1. 子程序退出,父程序不管不顧可能會造成殭屍程序的問題,而造成記憶體洩漏
  2. 一旦變成殭屍狀態,就很難殺死,kill -9也不行,因為誰也不能殺死一個死去的程序
  3. 父程序給子程序的任務完成的如何,我們需要指定
  4. 父程序通過等待的方式,回收子程序資源,獲取子程序資訊
  5. 為什麼要等待:一個是因為程序之間有競爭性!,另一個是避免產生殭屍程序

程序等待的方法

阻塞:為了完成一個功能發起函式呼叫,然後沒有完成這個功能則一直掛起等待,直到完成才返回。

非阻塞:為了完成一個功能發起一個函式,如果現在不具備完成的條件,則立即返回不等待。

wait方法

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *status);

// 返回值:成功返回被等待程序pid,失敗返回-1

//引數:輸出型引數,獲取子程序的退出狀態,不關心則設定為NULL

功能:等待子程序退出(阻塞式呼叫:如果沒有子程序退出,就一直等待不返回,直到子程序退出)

waitpid方法

pid_t waitpid(pit_t pid,int *status,int options);

//返回值:
        正常時候waitpid返回子程序的程序id
        如果設定了選擇WNOHANG,而呼叫waitpid發現沒有已退出的子程序可收集,則返回0
        如果調用出錯,則返回-1,這時候errno會被設定成相應的值以只是錯誤
//引數:
        pid:
            pid = -1,等待一個子程序,與wait等效
            pid > 0, 等待程序id == pid 的子程序退出
        status:
            用於獲取子程序退出狀態碼,不關心則設定為NULL
        options:
            選項引數(WNOHANG:如果沒有子程序退出,則立即報錯返回(非阻塞式呼叫),
                              如果有則回收資源)

          

功能:預設等待子程序退出 

  1. 如果子程序已經退出,呼叫wait或waitpid時,wait或waitpid會立即返回,並且釋放資源,獲得子程序退出狀態
  2. 如果任意時刻呼叫wait或waitpid,子程序存在其正常執行,則父程序可能阻塞
  3. 如果不存在該子程序,立即出錯返回

總結

wait:等待任意一個程序,若沒有子程序退出則一直等待(阻塞式,必須等到一個子程序退出後獲取退出狀態釋放資源才返回)

waitpid:可以等待指定的子程序退出,也可以等待一個任意的子程序退出(可以設定為非阻塞WNOHNAG)

 

獲取子程序status

  1. wait和waitpid,都有一個status引數,該引數是一個輸出型引數,由系統填充
  2. 如果傳遞NULL,表示不關心子程序的退出狀態資訊
  3. 否則,作業系統會根據該引數,將子程序的退出資訊反饋給父程序
  4. status不能當做整形來看待,可以當做點陣圖來看待(只研究低16位)

測試程式碼:
 

1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<errno.h>
  4 #include<string.h>
  5 #include<sys/wait.h>
  6 int main()
  7 {
  8     pid_t pid;
  9     pid = fork();
 10     if(pid == -1)
 11     {
 12         perror("fork");
 13         exit(1);
 14     }
 15     else if(pid = 0)
 16     {
 17         sleep(20);
 18         exit(10);
 19     }
 20     else
 21     {
 22        sleep(20);
 23         int st;
 24         int ret = wait(&st);
 25         if(ret > 0 && (st & 0x7F) == 0)//st&0x7F:與0x7F相當於取後7位,其餘值0
 26         {
 27             //正常退出:高8位代表退出狀態
 28             printf("child exit code:%d\n",(st>>8)&0x7F);
 29         }
 30         else if(ret >0)
 31         {
 32             //異常退出:低8位代表異常退出訊號
 33             printf("sig code:%d\n",st&0x7F);
 34         }
 35     }
 36     return 0;
 37 }

重開一個終端kill這個程序,就會產生假的異常退出。

kil -l 可以檢視訊號種類

總結:

  1. 雖然退出狀態用了4個位元組來獲取,但是實際只用了低16位的2個位元組儲存有用的資訊
  2. 在低16位中,高8位儲存子程序的退出碼,只有子程序執行完畢正常退出才有,低8位為0
  3. 低8位儲存引起異常退出的訊號值(第8位儲存core dump標誌),只有子程序異常退出是才有,這時高8位為0
  4. statu & 0x7 判斷是否正常退出,並獲取退出訊號
  5. statu >> 8 獲取退出碼
  6. WIFEXIT(是否正常退出,如果是返回ture)
  7. WEXITSTATUS可以來獲取WIFEXIT的退出碼

程序程式替換

替換原理

替換的是程式碼段所指向的程式碼實體記憶體區域

建立一個子程序大多時候,並不希望子程序做跟父程序相同的事情,而是希望執行一些其他的程式碼程式,這時候就用到了程序替換,程式替換隻是替換了程式碼段,初始化了資料區域,因此程式替換不會重新建立虛擬地址空間和頁表,只是替換了其中的內容,並且替換後子程序這個程序將從入口函式開始執行。

因為程式碼段被替換,因此在替換之後的原始碼不會再被執行,因為程式碼段中已經沒有這些程式碼

用fork建立子程序後執行的是和父程序相同的程式(但有可能是不同的程式碼分支),子程序往往呼叫一種exec函式以執行另一個程式,

當程序呼叫一種exec函式時,該程序的使用者空間程式碼和資料完全被新程式替換,從新程式的啟動例程開始執行。呼叫exec不建立新的程序,

所以前後程序的id並未改變。

替換函式

  1. 有6種以exec開頭的函式,統稱為exec函式
  2. execl
  3. execlp
  4. execle
  5. execv
  6. execvp
  7. execve

函式解釋與命名原理

  1. l(list):表示引數採用列表
  2. v(vector):引數用陣列
  3. p(path):有p自動搜尋環境變數PATH(不需要告訴路徑,只需要告訴檔名)
  4. e(env):表示自己維護環境變數
  5. 這些函式如果呼叫成功則載入新的程式從啟動程式碼開始執行,不在返回
  6. 如果調用出錯則返回-1
  7. 所以exec函式只有出錯的返回值而沒有成功的返回值
  8. 標頭檔案:<unistd.h>

總結

execl與execv區別:引數如何賦予(引數平鋪,指標陣列)

execl與execlp區別:

                             excl需要告訴作業系統這個程式的檔案全路徑

                             exclp不需要告訴路徑,只需要告訴檔名,會自動到PATH尋找

execl與excle區別:

                           execl繼承於父程序的環境變數

                           execle自己由我們使用者來組織環境變數

  1 #include<unistd.h>
  2 
  3 int main()
  4 {
  5     char *const argv[] = {"ps","-ef",NULL};
  6     char *const envp[] = {"PATH=/bin:/usr/bin","TERM=console",NULL};
  7     
  8     execl("/bin/ps","ps","-ef",NULL);
  9     
 10     execlp("ps","ps","-ef",NULL,envp);
 11     
 12     execv("/bin/ps",argv);
 13     
 14     execvp("ps",argv);
 15     
 16     execve("/bin/ps",argv,envp);
 17 return 0;
 18 }

事實上,只有execve值真正的系統呼叫,其他五個函式最終都呼叫execve,所以execve在man手冊第2節,其他函式在man第3節

關係如下圖所示:

簡單的shell

程式碼:

shell程式碼非常簡單隻能完成最基本的命令

優化程式碼:

實現一個簡單的shell - W_J_F_的部落格 - CSDN部落格 https://blog.csdn.net/W_J_F_/article/details/83618383

實現了

1:解決的無法識別多空格的問題(ls          -l)

2:解決的無任何輸入時回車死迴圈的問題

 

shell程式碼非常簡單隻能完成最基本的命令


 1 //自己實現一個簡單的shell
  2 #include<stdio.h>
  3 #include<unistd.h>
  4 #include<stdlib.h>
  5 #include<errno.h>
  6 #include<string.h>
  7 //1:獲取終端輸入
  8 //2:解析輸入(按空格解析到一個一個的命令引數)
  9 //3:建立一個子程序:在子程序中級進行程式替換,讓子程序執行命令
 10 //4:等待子程序執行完畢,收屍,獲取退出狀態碼
 11 
 12 int argc;
 13 char *argv[32];
 14 int param_parse(char *buff)
 15 {   
 16     if(buff == NULL)
 17         return -1;
 18     char *ptr = buff;
 19     char *tmp = ptr;
 20     
 21     argc = 0;
 22     while((*ptr) != '\0')
 23     {   
 24         if((*ptr) == ' '&& *(ptr + 1) != ' ')
 25         {   
 26             //當遇見空格且下一個位置不是空格
 27             //將空格置為‘\0’
 28             //但是我們使用argv[argc]來儲存這個字串位置
 29             *ptr = '\0'; 
 30             argv[argc] = tmp;
 31             tmp = ptr + 1;
 32             argc++;
 33         }
 34         ptr++;
 35     }
 36     argv[argc++] = tmp;
 37     argv[argc] = NULL;
 38     return 0;
 39 }
 40 int do_exec()
 41 {
 42     int pid = 0;
 43     pid = fork();
 44     if(pid < 0)
 45     {
 46         perror("fork");
 47         return -1;
 48     }
 49     else if (pid == 0)
 50     {
 51         execvp(argv[0],argv);
 52         exit(0);
 53     }
 54     //父程序在這裡必須等待子程序退出,來觀察子程序為什麼會退出
 55     //是否出現了什麼錯誤,通過獲取狀態碼,並且轉換退出碼所對應
 56     //的錯誤資訊進行列印
 57     int statu;
 58     wait(&statu);
 59     //判斷子程序是否程式碼執行完畢退出
 60     if(WIFEXITED(statu))
 61     {
 62         //獲取到子程序的退出碼,轉換為文字資訊
 63         printf("%s",strerror(WEXITSTATUS(statu)));
 64     }
 65     return 0;
 66 }
 67 
 68 int main()
 69 {
 70     while(1)
 71     {
 72         printf("shell> ");
 73         char buff[1024] = {0};
 74         scanf("%[^\n]%*c",buff);
 75         //%[^\n] 獲取資料直到遇見\n為止
 76         //%*c    清空緩衝區,資料都不要(不然還存有是上一個\n)
 77         printf("%s\n",buff);
 78         param_parse(buff);
 79         do_exec();
 80     }
 81     return 0;
 82 }

圖解

時間軸表示發生次序,shell從使用者讀入字串‘ls’,shell建立一個新的程序,然後在那個程序中執行ls並等待那個程序結束

然後shell讀取新的一行輸入,建立一個新的程序,在這個程序中執行程式,並等待這個程式結束

所以寫一個shell,需要迴圈一下過程

  1. 獲取命令
  2. 解析命令
  3. 建立一個子程序fork,並進行程序替換execvp
  4. 父程序等待子程序的退出wait

總結

一個從程式有很多函式組成,一個函式具有呼叫另一個函式,同時傳遞給它一些引數。

被呼叫的函式執行一定的操作,然後返回一個值,每個函式都有他的區域性變數,不同的是

函式通過call/return系統進行通訊,這種通過引數和返回值在擁有私有資料的函式之間通訊的模式

是結構化程式設計的基礎。如下圖:

一個c程式可以fork/exec另一個程式,並給它傳一些引數,這個被呼叫的程式執行一定的操作

然後通過exit(n)產生返回值,呼叫它的程序可以通過wait(&ret)來獲取exit的返回值