【Linux】進程控制

2024年2月6日 19点热度 0人点赞

一、進程創建

1. fork 函數創建子進程

Linuxfork 函數是非常重要的函數,它從已存在進程中創建一個新進程。新進程為子進程,而原進程為父進程。我們在前面的學習中也遇到過,所以在此簡單介紹一下。

#include <unistd.h> // 頭文件
pid_t fork(void);
返回值:子進程中返回0,父進程返回子進程 id,出錯返回 -1.

當進程調用 fork,控制轉移到內核中的 fork 代碼後,內核應該做:

  1. 分配新的內存塊(pcb)和內核數據結構(進程地址空間、頁表等)給子進程
  2. 將父進程部分數據結構內容拷貝至子進程
  3. 將子進程添加到系統進程列表當中
  4. fork 返回,開始調度器調度

當父進程創建子進程後,fork 之後父子進程代碼共享,可以使用 if else 進行分流,讓子進程和父進程執行不同的任務。

2. 寫時拷貝

我們知道,當父進程創建子進程後,操作系統會將父進程的 pcb、進程地址空間、頁表等拷貝一份給子進程;那麼當子進程想要修改數據的時候,我們知道會發生寫時拷貝,那麼操作系統怎麼會知道什麼時候進行寫入拷貝呢?操作系統又如何介入這個操作呢?

其實操作系統在父進程創建子進程之前,會將頁表中的訪問權限字段統一修改成隻讀,無論是地址空間中的哪個區域,都會改成隻讀,然後再創建子進程,這是為什麼呢?

此時我們用戶是不知道的,這是為了讓操作系統發現我們要進行寫時拷貝,此時我們子進程正在寫入,但是這個區域是隻讀區域,頁表轉換會因為權限問題出錯,操作系統就會介入這個過程;首先操作系統會檢查這個區域是否真的是隻讀區域,還是在拷貝給子進程時自己修改成隻讀的區域,如果真的是隻讀區域,此時就會報錯;但是操作系統如果檢查出是自己修改的隻讀區域,就證明不是出錯,就會觸發進行重新申請內存,拷貝內容的策略機制。

可以結合下圖進行理解:

子進程修改內容前:

子進程修改內容後:

在寫時拷貝完成後,操作系統會將對應修改內容的頁表中的訪問權限字段修改成讀寫(rw),所以說,在子進程沒有進行寫入的時候,頁表中的訪問權限字段都是隻讀!

另外一個問題,操作系統進行寫時拷貝時,為什麼要進行拷貝呢?直接將數據寫入不就好了嗎?原因是因為我們可能不需要修改這個數據的所有內容,可能隻需要修改一部分內容!

3. 創建一個多進程

下面我們實現一個代碼,創建一個多進程,代碼如下:

      1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <stdlib.h>
      4 
      5 typedef void (*callback)();   // 函數指針
      6 
      7 void work()
      8 {
      9     int cnt = 5;
     10     while(cnt--)
     11     {
     12         printf("i am child process, pid:%d, ppid:%d\n", getpid(), getppid());
     13         sleep(1);
     14     }
     15 }
     16 
     17 void createSubProcess(int n, callback cb)
     18 {                                                                                                                                 
     19     int i = 1;
     20     for(; i <= n; i  )
     21     {
     22         sleep(1);
     23         pid_t id = fork();
     24         if(id == 0)
     25         {
     26             printf("create child success: %d\n", i);
     27             cb();       // 回調函數的使用                                                                                                     
     28             exit(0);   // 退出子進程
     29         }
     30     }
     31 }
     32 
     33 int main()
     34 {
     35     createSubProcess(5, work);
     36     
     37     // 隻有父進程會走到這
     38     return 0;
     39 }

如上,createSubProcess 是一個創建子進程的函數,我們可以通過傳入參數 n,代表需要創建子進程的個數;cb,需要執行的函數,即子進程的任務,以達到我們的目的。

二、進程終止

1. 進程退出場景

  • 代碼運行完畢,結果正確
  • 代碼運行完畢,結果不正確
  • 代碼異常終止

下面我們逐一分析上面進程退出的三種場景。

main 函數的返回值

  • 代碼正常運行

我們通常寫代碼中,main 函數都是要返回一個 int 類型的值的,如下:

    int main()
    {
      return 0;
    }

那麼為什麼需要返回一個值呢?因為 main 函數也是被調用的,被誰調用我們先不關心,重要的是我們需要把代碼是否正常運行的結果返回,成功則返回 0,失敗返回非 0.

下面我們嘗試一下在 main 函數中返回非 0;其中我們的 main 函數是在一個程序中的,該程序運行起來就是一個進程,而且是 bash 的子進程,所以該進程最終會給 bash 返回 main 的返回值;我們可以使用指令 echo $? 查看最近一次進程退出的結果;如下段代碼:

    int main()
     {                                                                                                                                 
         return 1;
     }  

其中 ? 中保存的是最近一個子進程執行完畢時的退出碼,$ 相當於解引用操作。

我們運行起來之後查看它的退出結果:

如上圖就把 main 函數的退出結果打印出來了,其實這個結果就是 main 函數的退出碼!所以 main 函數的返回值就是進程的退出碼! 0代表成功,非0代表失敗。

而退出碼當中,0 代表成功,但是當退出碼為非 0 的時候,"我們"需要關心它是為什麼失敗的,這個"我們"指的是父進程;所以這時候就應該有不同的數字表明不同的原因,比如 1 代表某種失敗原因,2 也代表另一種失敗原因… 所以每一個數字代表不同的錯誤,這就叫做退出碼

純數字雖然能表明錯誤原因,但是不便於我們閱讀,所以應該需要有一些能夠將數字轉換為退出碼的字符串描述方案;所以系統默認已經為我們提供了一些接口,能夠將數字轉換為不同的出錯原因,方便我們去查!當然我們也可以自定義去定義每個數字對應的錯誤原因是什麼。而系統提供的接口就是 strerror(),我們查看一下這個接口:

返回值則是對應退出碼的字符串;下面我們打印一下這個接口中的字符串,如下段代碼:

      1 #include <stdio.h>  
      2 #include <unistd.h>  
      3 #include <stdlib.h>  
      4 #include <string.h>                                                                                                               
      5                                         
      6 typedef void (*callback)();             
      7                                         
      8 int main()                              
      9 {                                       
     10     int i = 0;                          
     11     for(; i < 200; i  )                 
     12     {                                        
     13         printf("%d: %s\n", i, strerror(i));                                                                    
     14     }                                                                                                          
     15     return 0;                                                                                                  
     16 } 

因為裡面的字符串太多,大傢可以自行打印觀察結果,其中裡面一共有 134 個字符串,即每個數字對應的錯誤原因。

但是我們的 Linux 中並不使用系統提供的接口獲取退出碼的退出原因描述,而是使用自定義的退出原因描述。

  • 錯誤碼

我們在程序中可能會調用多個庫函數或者接口,但調用它們的時候可能也會出錯,出錯的時候就會設置一個錯誤碼,即 errno,它會記錄我們的程序中最後一次庫函數或者系統接口出錯的錯誤碼;註意這個錯誤碼是 C 語言為我們提供的。

那麼錯誤碼和退出碼有什麼關系呢?

  1. 錯誤碼通常是衡量一個庫函數或者是一個系統調用一個函數的調用情況
  2. 退出碼通常是一個進程退出的時候它的退出結果

它們的共同特點都是,當失敗的時候,來衡量函數、進程出錯時的出錯詳細原因。

例如下段代碼:

      1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <stdlib.h>
      4 #include <string.h>
      5 #include <errno.h>
      6 
      7 
      8 
      9 int main()
     10 {
     11     printf("before: %d\n", errno);
     12     FILE *fp = fopen(".sadsadsa.txt", "r");
     13     if(fp == NULL)
     14         printf("after: %d, error string : %s\n", errno, strerror(errno));                                                         
     15     return 0;
     16 }

我們在代碼中打開一個不存在的文件,肯定是會失敗的,這時候我們就可以利用這個函數接口的錯誤碼去找到對應的錯誤原因描述。

以上就是代碼正常運行的情況,不管結果對不對。

  • 代碼異常終止

首先我們要知道,代碼異常終止其實是代碼並沒有跑完,退出碼也就沒有意義;所以我們在這先引出一下異常問題,簡單介紹一下,後面我們會詳細學習。

異常問題:

一旦我們的代碼發生了異常,我們觀察一下會發生什麼現象,例如我們對 NULL 解引用修改,根據我們前面學的知識,NULL 是在 0 地址處的,也就是在代碼區,不可被修改,所以這個代碼是異常的,如下:

     int main()
{
         int* p = NULL;
         *p = 10;  
         return 0;
     }

我們運行一下觀察會有什麼現象:

如上圖,系統給我們報了一個段錯誤;我們常說這樣的現象叫做程序崩潰,但是本質上這個程序運行起來它就是一個進程,其實就是進程被異常終止了,那麼這個進程是被誰殺掉的呢?進程異常是被操作系統殺掉的,因為操作系統是進程的管理者!那麼操作系統是如何殺掉進程的呢?

當一個進程一旦出異常了,操作系統本質上是通過信號的方式殺掉進程的;我們以前學進程概念的時候學過使用 kill -9 pid 殺掉進程,我們繼續看看更多的信號,可以使用 kill -l 指令,如下:

其實當我們的進程出異常的時候,進程的異常信息會被操作系統檢測到,進而被操作系統轉化為信號然後把該進程殺掉的!例如我們上面那段代碼中的段錯誤,是可以在上面的信號中找到的,例如下圖:

如上圖,11 號信號就是段錯誤對應的信號,也就是說該進程接收到操作系統給它發的 11 號信號而終止的!怎麼證明呢?下面我們寫一段正常的代碼,然後在另外一個窗口給該進程發送對應的信號觀察一下:

    int main()
    {
        while(1)
        {
            printf("i am a normal process: %d\n", getpid());
            sleep(1);
        }
        return 0;
    }

例如上段代碼是正常的代碼,我們運行起來,在另外一個窗口給它發送 11 號信號:

如上圖,正常運行的代碼接收到 11 號信號確實會異常終止了。我們觀察到對應的信號中沒有 0 號信號,其實 0 號信號就是正常的情況。

所以一個進程首先要先檢查代碼是否異常終止了,是否異常終止隻需要看有沒有接收到信號即可,而接收信號無非就是接收一個數字;當代碼沒有異常正常運行時,我們就要看該進程的退出碼,觀察它是否正確運行,而退出碼無非也是一個數字而已;所以一個進程是否能正常並且正確運行,父進程隻需要觀察這兩個數字即可!即信號和退出碼!

2. 進程常見的退出方法

(1)從 main 返回

從上面的學習中我們知道,main 函數的返回值就是退出碼,所以我們可以通過 main 函數直接返回從而進程退出,這個不多說;但是進程退出不能通過其它子函數返回,隻能通過 main 函數返回。

(2)exit

exit 是庫函數,也是退出進程的常見方法,它和 return 的使用差不多,直接在程序的任意位置使用,並在括號內填入退出碼即可;下面看一段代碼:

      1 #include <stdio.h>
      2 #include <sys/types.h>
      3 #include <unistd.h>
      4 #include <stdlib.h>
      5 
      6 void func()
      7 {
      8     int cnt = 5;
      9     while(cnt--)
     10     {
     11         printf("i am a process, pid: %d, ppid: %d\n", getpid(), getppid());
     12         exit(7);                                                                                                                  
     13     }
     14 }
     15 
     16 int main()
     17 {
     18     func();
     19 
     20     return 0;
     21 }

如上,程序應該是在調用 func 後執行一次 printf 後直接退出,我們觀察結果是否是我們預期的結果:

如上圖,確實是這樣的,我們再觀察一下退出碼:

也確實是 7,所以 exit 的使用和 從 main 中 return 差不多。但是它與我們下面要介紹的 _exit 有區別。

(3)_exit

_exit 是系統調用,_exit 也同樣可以在程序的任意位置終止進程,我們先看一下使用:

      1 #include <stdio.h>
      2 #include <sys/types.h>
      3 #include <unistd.h>
      4 #include <stdlib.h>
      5 
      6 void func()
      7 {
      8     int cnt = 5;
      9     while(cnt--)
     10     {
     11         printf("i am a process, pid: %d, ppid: %d\n", getpid(), getppid());
     12         _exit(7);                                                                                                                 
     13     }
     14 }
     15 
     16 int main()
     17 {
     18     func();
     19 
     20     return 0;
     21 }

還是上面那段代碼,我們將 exit 改成 _exit ,觀察現象和它的退出碼:

如上圖,它是可以正常退出的;

退出碼也是正常的;那麼它和 exit 的區別在哪呢?下面我們將上面那段代碼的 printf 中的換行符去掉,隻改下面這一句,觀察現象:

  printf("i am a process, pid: %d, ppid: %d", getpid(), getppid());

此時沒有打印出任何東西,我們再將代碼中的 _exit 改回 exit 觀察一下:

如上圖,如果是 exit 的話沒有換行符也是可以正常打印出結果的,這是為什麼呢?

所以我們得出結論:exit 終止程序的時候,會自動刷新緩沖區,所以就會打印出我們要的結果;_exit 終止程序的時候,不會自動刷新緩沖區,它會直接退出進程,什麼也不管,所以不會打印出結果。

三、進程等待

1. 進程等待概念

進程等待就是通過 wait/waitpid 的方式,讓父進程對子進程進行資源回收的等待過程。

2. 進程等待必要性

進程等待的必要性有以下幾點:

  1. 解決子進程僵屍問題帶來的內存泄漏;另外,進程一旦變成僵屍狀態,那就刀槍不入,kill -9 也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。
  2. 父進程創建子進程是要讓子進程完成相應的任務,子進程完成得如何得讓父進程知道,所以需要通過進程等待的方式,獲取子進程退出的信息;而獲取子進程退出的信息我們上面學過退出碼是否有接收到信號,所以隻需要獲取這兩個數字的信息即可,但這不是必須的,可是系統也需要提供這樣的接口讓我們獲取。

3. 進程等待的方法

  • wait

我們可以看一下 man 手冊中的 wait,其中 wait 是系統調用,所以它所在的手冊是 2 號手冊,3 號手冊是庫函數,所以我們執行指令 man 2 wait 即可查看 wait 系統調用:

wait 的作用是等待父進程的任意一個子進程的退出。

其中 wait 中的參數 status 我們先不管,我們在下面介紹 waitpid 再介紹;下面我們看一下它的返回值:

如上,wait 的返回值:如果成功,返回的是退出的子進程的 pid;失敗則返回 -1.

下面我們看一段代碼,驗證父進程是否會等待子進程並當子進程為僵屍狀態時是否會回收子進程的資源:

       #include <stdio.h>
       #include <sys/types.h>
       #include <unistd.h>
       #include <stdlib.h>
       #include <sys/wait.h>
       void worker()
{
           int cnt = 5;
          while(cnt--)
          {
              printf("i am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
              sleep(1);
          }
       }
      int main()
{
          pid_t id = fork();
          if(id == 0)
          {
              //child
              worker();
              exit(0);
          }
          else 
          {
              sleep(10);                                                            
              //father
              pid_t rid = wait(NULL); 
              if(rid == id)
              {                      
                  printf("wait success, pid: %d\n", getpid());
              }
              sleep(5);            
          }    
          return 0;                                           
      }     

如上代碼,我們先使用 fork 創建子進程,讓子進程去執行 worker 方法,父進程則 sleep 上10秒,因為執行 worker 方法需要 5 秒,所以 5 秒後子進程會變成僵屍狀態,因為此時父進程還在 sleepsleep 過後我們的預期是父進程會回收子進程,最後再 sleep 上 5 秒,便於我們觀察結果:

如上圖,結果確實如此,當子進程變為僵屍狀態父進程確實會回收子進程。

那麼在子進程運行期間,父進程有沒有調用 wait 呢?父進程在幹什麼呢?下面我們通過下面這段代碼不再讓父進程 sleep,驗證一下:

       #include <stdio.h>
       #include <sys/types.h>
       #include <unistd.h>
       #include <stdlib.h>
       #include <sys/wait.h>
       void worker()
{
           int cnt = 5;
          while(cnt--)
          {
              printf("i am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
              sleep(1);
          }
       }
      int main()
{
          pid_t id = fork();
          if(id == 0)
          {
              //child
              worker();
              exit(0);
          }
          else 
          {
              printf("wait before\n");                                                           
              //father
              pid_t rid = wait(NULL); 
              printf("wait after\n");
              if(rid == id)
              {                      
                  printf("wait success, pid: %d\n", getpid());
              }
              sleep(5);            
          }    
          return 0;                                           
      }   

執行結果如下:

如上圖,我們得出結論:如果子進程根本就沒有退出,父進程必須在 wait 上進行阻塞等待,直到子進程僵屍,wait 自動回收,再返回!

同時,一般而言誰先運行不知道,但是最後一般都是父進程最後退出。

  • waitpid

我們先看一下 waitpid 的手冊介紹:

如上圖,waitpid 的第一個參數 pid 是指 waitpid 可以等待任意一個進程,如果 pid >= 0 ,則等待進程 id 為指定 pid 的進程;如果 pid == -1,則和 wait 一樣,等待任意一個進程。其中 waitpid 的返回值和 wait 的返回值一模一樣,大於0表示成功,返回的是等待的進程id;失敗則返回小於 0 的數;第三個參數 options 我們暫時先不管,讓它以默認的等待方式,即 0;第二個參數 status 稍後介紹。

下面我們使用 waitpid 替代 wait,再次演示一下上面的操作;其中第二個參數一樣先設為空,因為我們還暫時還不關心它的退出結果;代碼如下:

       #include <stdio.h>
       #include <sys/types.h>
       #include <unistd.h>
       #include <stdlib.h>
       #include <sys/wait.h>
       void worker()
{
           int cnt = 5;
          while(cnt--)
          {
              printf("i am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
              sleep(1);
          }
       }
      int main()
{
          pid_t id = fork();
          if(id == 0)
          {
              //child
              worker();
              exit(0);
          }
          else 
          {
              printf("wait before\n");                                                           
              //father
              pid_t rid = waitpid(id, NULL, 0); 
              printf("wait after\n");
              if(rid == id)
              {                      
                  printf("wait success, pid: %d\n", getpid());
              }
              sleep(5);            
          }    
          return 0;                                           
      }   

結果如下:

如上圖,結果也符合我們的預期。

接下來我們介紹一下 waitpid 的第二個參數 statuswaitpid 的第二個參數 status 是一個輸出型參數,我們通過 waitpid 的系統接口將這個參數傳給操作系統,操作系統會將這個參數寫入然後給我們返回這個進程的退出信息,我們可以根據以上結論嘗試一下使用;其中代碼如下,worker 還是上面的 worker

      int main()
{
          pid_t id = fork();
          if(id == 0)
          {
              //child
              worker();
              exit(10);
          }
          else 
          {
              printf("wait before\n");                                                           
              //father
              int status = 0;
          pid_t rid = waitpid(id, &status, 0); 
              printf("wait after\n");
              if(rid == id)
              {                      
            printf("wait success, pid: %d, status: %d\n", getpid(), status);
              }
              sleep(5);            
          }    

為了更明顯地看到結果,我們將子進程的退出碼改為 10,下面我們觀察 status 返回的結果:

如上圖,為什麼退出信息不是 10 呢?為什麼會是 2560 呢?下面我們就要介紹一下 status 的構成了;首先 status 是一個整數,它有 32 位比特位,它是根據 32 位比特位進行區域劃分的,使用不同的比特位區域來表示不同的含義的!而我們在後續使用的時候,我們隻考慮 status 比特位的低 16 位,其中它的區域劃分如下圖,將它歸為兩大類:

如上圖,如果進程是正常終止,那麼 0 ~ 6 位都是 0,表示沒有接收到信號;8 ~ 15 位表示退出狀態,即退出碼;如果是被信號所殺,那麼低位保存的是終止信號的信息,還有一個第 7 位表示 core dump 的標志位,我們先不管,以後再學。

這就是為什麼我們將子進程的退出碼設為 10,但是 status 整體打印出來是 2560 的原因了,我們可以按照上面的原理分析一下,因為我們上面的代碼是正常退出的,並沒有被信號所殺,所以按照第一種情況分析,如下圖:

下面我們將代碼改進一下,觀察是否是像我們預期的結果一樣:

      int main()
      {
          pid_t id = fork();
          if(id == 0)
          {
              //child
              worker();
              exit(10);
          }
          else 
          {
              printf("wait before\n");                                                           
              //father
              int status = 0;
          pid_t rid = waitpid(id, &status, 0); 
              printf("wait after\n");
              if(rid == id)
              {                      
            printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7f, (status)>>8&0xff);
              }
              sleep(5);            
          }    

如上代碼,如果我們需要打印 status 中的退出碼,應該是要將 status 右移 8 位後按位與上 0xff(1111 1111),即可得到退出碼;如果我們需要得到退出信號的信息,則直接按位與 0x7f(0) 就將除了低 7 位的其它位都清零了,隻保留低 7 位;所以執行結果如下:

如上圖,結果確實是我們預期的結果,即代碼跑完,結果不正確。

如果被信號所殺的呢?下面我們也演示一下被信號所殺 status 的信息:

如上圖,結果沒有問題,exit sig 顯示的是對應的信號編號。那為什麼 exit code0 呢?因為當代碼異常了(被信號所殺),那麼 exit code 就沒有意義了,所以有可能是全 0,也有可能是隨機值,但是已經沒有意義了。

那麼父進程是如何得知子進程的退出信息的呢?首先我們在用戶層面調用 wait/waitpid 接口的時候,定義了一個 status 變量並將它傳入接口中,操作系統內部會有一個指針指向 status;而數據和代碼存在於進程 pcb 中,當數據和代碼執行完,pcb 中有兩個變量,分別是 exit_codeexit_signal,操作系統會將這兩個變量通過位運算的方式寫入到指向 status 的指針中,然後返回結果到用戶層面,我們就能得到 status 了。

那父進程又是如何等待子進程的呢?首先每個進程 pcb 內部都有內置的等待隊列,當父進程在等子進程時,其實就是將父進程的 pcb 鏈入子進程的等待隊列裡;當子進程退出時,操作系統就直接從子進程的等待隊列裡把父進程拿出來,然後調度父進程,就執行父進程對應的 wait/waitpid 了。

但是我們通過位運算得到的退出信息可讀性不是很好,所以 Linux 也為我們提供了兩個接口:

  1. WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真。(查看進程是否是正常退出)
  2. WEXITSTATUS(status): 若 WIFEXITED 非零,提取子進程退出碼。(查看進程的退出碼)

所以我們上面的代碼可以使用上面兩個接口改成如下:

      int main()
{
          pid_t id = fork();
          if(id == 0)
          {
              //child
              worker();
              exit(10);
          }
          else 
          {
              printf("wait before\n");                                                           
              //father
              int status = 0;
          pid_t rid = waitpid(id, &status, 0); 
              printf("wait after\n");
              if(rid == id)
              {                      
            if(WIFEXITED(status))
            {
              printf("child process normal quit, exit code: %d\n", WEXITSTATUS(status));
            }
            else
            {
              printf("child process quit except!\n");                                                                                      
            }
              }
              sleep(5);            
          }    

當代碼正常退出時,如下:

  • 等待多個子進程

下面我們寫一段等待多個子進程的代碼:

       void worker(int num)
       {
           int cnt = 10;
          while(cnt--)
          {
              printf("i am child process, pid: %d, ppid: %d, cnt: %d, num: %d\n", getpid(), getppid(), cnt, num);
              sleep(1);
          }
       }

首先我們循環創建多個子進程,讓子進程執行 worker 方法,再傳入參數 i,是為了讓每個子進程都有自己的編號,方便我們觀察結果;最後父進程也是要循環進行等待的;所以代碼如下:

      int main()
{
          for(int i = 0; i < n; i  )
          {
              pid_t id = fork();
              if(id == 0)
              {
                  worker();
                  exit(i);
              }
          }
          // 等待多個子進程                                                           
          for(int i = 0; i < n; i  )                                                  
          {                 
              int status = 0;                                                          
              pid_t rid = waitpid(-1, &status, 0); // pid == -1,等待任意一個退出的子進程
              if(rid > 0)                                                                                                    
              {
                   printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));        
              }                                                   
          }
          return 0;
      }   

最後執行的結果會很多,因為創建了很多子進程,所以我們隻看最終的等待結果,如下圖:

如上圖,我們是按循環順序創建的子進程,為什麼等待結果的退出碼不是從 0 到 9 的呢?因為進程在調度運行的時候是沒有規律的,完全由操作系統決定。

最後,為什麼我們不用全局變量獲取子進程的退出信息,而是用系統調用呢?原因是因為進程之間具有獨立性,父進程是無法直接獲取子進程的退出信息!

  • waitpid 的第三個參數

waitpid 的第三個參數 options 有兩種狀態,分別是:

0:阻塞等待
WNOHANG:等待的時候,以非阻塞的方式等待

阻塞式等待:子進程不退出,wait/waitpid 不返回;
非阻塞式等待:如果等待條件不滿足,
wait/waitpid 不阻塞,而是立即返回;

非阻塞等待中,當父進程檢測到子進程還沒就緒,即等待條件不滿足時,往往要進行重復調用,重復檢測子進程的狀態,這也叫輪詢;其中輪詢 非阻塞方案叫做非阻塞輪詢方案進行等待;那麼這樣的好處是什麼呢?好處就是當父進程在等待的過程中,可以做一些自己的占據時間並不多的事情!而阻塞等待中父進程什麼都做不了!

所以我們重新看 waitpid 的返回值,其中 > 0 是等待成功,子進程也退出了,返回的是子進程的 pid== 0 等待是成功的,但是子進程還沒有退出;< 0 是等待失敗。

下面我們寫一個以非阻塞方式等待的代碼:

      1 #include <stdio.h>
      2 #include <sys/types.h>
      3 #include <unistd.h>
      4 #include <stdlib.h>
      5 #include <sys/wait.h>
      6 
      7 
      8  
      9 #define TASK_NUM 5
     10 
     11 // 定義一個函數指針
     12 typedef void (*task_t)();
     13 
     14 // 
     15 //父進程在等待時需要執行的任務
     16 void download()
     17 {
     18     printf("this is a download task is rnning!\n");
     19 }
     20 void printLog()                                                                                                                              
     21 {
     22     printf("this is a write log task is rnning!\n");
     23 }
     24 void show()
     25 {
     26     printf("this is a show info task is rnning!\n");
     27 }                                                                                                                                            
     28 ///
     29 
     30 
     31 // 對函數指針數組進行初始化
     32 void initTasks(task_t tasks[], int num)
     33 {
     34     for(int i = 0; i < num; i  ) tasks[i] = NULL;
     35 }
     36 
     37 // 對函數指針數組進行添加任務
     38 int addTask(task_t tasks[], task_t t)
     39 {
     40     int i = 0;
     41     for(; i < TASK_NUM; i  )
     42     {
     43         if(tasks[i] == NULL)
     44         {
     45             tasks[i] = t;
     46             return 1;
     47         }
     48     }
     49     return 0;
     50 }
     51 
     52 // 執行任務
     53 void executeTask(task_t tasks[], int num)
     54 {                                                                                                                                            
     55     for(int i = 0; i < num; i  )
     56     {
     57         // 如果對應的函數指針不為空,就調用它
     58         if(tasks[i]) tasks[i]();
     59     }
     60 }
     61 
     62 // 子進程執行
     63 void worker(int cnt)
     64 {
     65     printf("I am child, pid: %d, cnt: %d\n", getpid(), cnt);
     66 }
     67 
     68 int main()
     69 {
     70     task_t tasks[TASK_NUM];
     71     initTasks(tasks, TASK_NUM);
     72     addTask(tasks, download);
     73     addTask(tasks, printLog);
     74     addTask(tasks, show);
     75 
     76     pid_t id = fork();
     77     if(id == 0)
     78     {                                                                                                                                        
     79         // child
     80         int cnt = 10;
     81         while(cnt)
     82         {
     83             worker(cnt);
     84             sleep(2);
     85             cnt--;
     86         }
     87 
     88         exit(0);
     89     }
     90 
     91     while(1)
     92     {
     93         //father
     94         int status = 0;
     95 
     96         // 非阻塞等待,可以讓等待方在返回的時候,順便做做自己的事情
     97         pid_t rid = waitpid(id, &status, WNOHANG);
     98         if(rid > 0)
     99         {                                                                                                                                    
    100             // wait success, child quit now;
    101             printf("child quit success, exit code: %d, exit signal: %d\n", (status>>8)&0xFF, status&0x7F);
    102             break;
    103         }
    104         else if(rid == 0)
    105         {
    106             printf("##################################################\n");
    107             // wait success, but child not quit
    108             printf("child is alive, wait again, father do other thing....\n");
    109             // 該函數內部,其實是回調式執行任務
    110             executeTask(tasks, TASK_NUM); // 也可以在內部進行自己移除&&新增對應的任務
    111             printf("##################################################\n");
    112         }
    113         else
    114         {
    115             // wait failed, child unknow
    116             printf("wait failed!\n");
    117             break;
    118         }
    119 
    120         sleep(1);
    121     }
    122     return 0;
    123 }

如上就是以非阻塞方式等待子進程的代碼,大傢下去可以自行驗證。

四、進程程序替換

以前我們所創建的所有子進程,執行的代碼,都是父進程代碼的一部分;而從現在開始,我們可以做到讓子進程執行新的程序,執行全新的代碼和訪問全新的數據,不再和父進程有關系!這就是進程程序替換。

1. 單進程的程序替換

首先我們先使用單進程熟悉一下程序替換,熟悉程序替換首先就要熟悉它的接口函數,而這種函數稱為替換函數,我們可以使用指令 man execl 查看相關的函數:

如上圖,有六種以 exec 開頭的函數,統稱 exec 函數。

下面我們先嘗試使用一個 execl 函數,如下代碼:

      1 #include <stdio.h>
      2 #include <unistd.h>
      3 
      4 int main()
      5 {
      6     printf("pid: %d, exec command begin\n", getpid());
      7     execl("/usr/bin/ls", "ls", "-a", "-l", NULL);                                                                                 
      8     printf("pid: %d, exec command end\n", getpid());
      9     return 0;
     10 }

執行結果如下:

如上圖,我們的執行程序是變成了一個進程了的,因為在開始的時候有 pid,而後面使用了 execl 函數之後,我們的進程程序實際上是被 execl 括號內的程序替換了,所以沒有執行到下一句的 printf 的語句打印。所以這就能充分說明了,我們可以使用 “語言” 調用其它程序!

下面我們開始介紹一下 execl 這個函數的參數:

其中 path 是我們需要替換的程序,想要找到這個程序,首先要找到程序文件的路徑,所以第一個參數 path 是需要替換的程序的路徑;arg 是如何執行的問題,我們在命令行怎麼寫,就將這個參數怎麼傳。我們可以看到後面還有一些 ,這就像printf 函數的可變參數一樣,後面的參數可以傳很多個,但是這裡是需要我們傳這個程序對應的選項,如 ls 指令後面可以跟很多選項,我們就以空格為分隔符,在 execl 中以字符串形式傳入即可;最後必須以NULL結尾,表示參數傳遞完畢。

下面我們開始介紹 exec 系列函數的原理,首先我們的可執行程序運行起來,變成一個進程,生成 pcb虛擬地址空間頁表等等,將我們程序的代碼和數據映射到物理內存中,如下圖:

當我們調用了 exec 系列的函數後,假設我們以上面的 ls 為例,當我們使用 ls 的程序替換我們的程序時,磁盤上的 ls 程序的數據和代碼會替換我們原來程序在物理內存中的數據和代碼,當 cpu 繼續調度我們的進程時,就會執行 ls 的程序,所以我們原來程序不再會被執行;結合下圖理解:

2. 多進程的程序替換

下面我們創建一個子進程來進行程序替換,如下代碼:

      1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <sys/wait.h>
      4 #include <sys/types.h>
      5 
      6 int main()
      7 {
      8     pid_t id = fork();
      9     if(id == 0)
     10     {
     11         // child
     12         printf("pid: %d, exec command begin\n", getpid());
     13         sleep(3);
     14         execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
     15         printf("pid: %d, exec command end\n", getpid());
     16     }
     17     else
     18     {                                                                                                                             
     19         // father
     20         pid_t rid = waitpid(-1, NULL, 0);
     21         if(rid > 0)
     22         {
     23             printf("success wait, rid: %d\n", rid);
     24         }
     25     }
     26     return 0;
     27 }

如上代碼,我們讓子進程進行程序替換,讓父進程等待子進程,執行結果如下:

如上圖,我們觀察到子進程還是會繼續進行程序替換,而父進程也成功回收了子進程。那麼為什麼子進程進行程序替換不會影響父進程呢?因為我們前面學過,進程之間具有獨立性!

當父進程創建子進程後,父子進程共享代碼和數據,但是當子進程進行程序替換的時候,物理內存的數據和代碼會被修改覆蓋,所以這時候會影響父進程,所以這時候會發生寫時拷貝,將數據和代碼拷貝一份給子進程後,將子進程的數據和代碼替換即可,這時候再修改子進程頁表的映射關系即可!不會影響父進程!

  • 問題1

雖然我們懂了程序替換的原理,但是還是會延申一系列的問題,例如,子進程怎麼知道要從替換的新程序的最開始執行呢?它怎麼知道最開始執行的地方在哪裡?

首先我們在編譯形成可執行程序的時候,它不是雜亂無章的把代碼和數據隨便放的,而是有自己放置的規則的,也就是可執行程序是有自己的格式的;而在這個可執行程序的頭部裡面,有一個字段 entry,這個字段存的就是可執行程序的入口地址;而我們在調用 exec 系列的函數的時候,它會直接獲取這個可執行程序的頭部信息 entry.

而在每個進程中,都有一個程序計數器 eip,實質是個寄存器,寄存器雖然隻有一個,但是寄存器中的內容是每個進程獨有的,所以說每個進程中都私有一個 eip;當進程切換時它可以把自己的 eip 放上來,就可以知道當前自己執行到哪一行代碼了,因為 eip 中存的是當前執行指令的下一條指令的地址;所以當進行程序替換的時候,子進程獲取到新程序頭部字段 entry,將這個字段的地址填入到子進程的 eip 中,子進程就可以從新程序的入口開始執行了。

  • 問題2

我們上面執行的單進程和多進程代碼中,都沒有看見結果打印 exec 之後的 printf 語句,這是為什麼呢?原因就是隻要 exec 系列函數替換成功了,eip 就會轉換過去執行新程序的代碼了,也就是說 exec 之後的代碼都不會被執行了。

所以 exec 這樣的函數,如果當前進程執行成功了,則後續代碼沒有機會執行了,因為被替換了!所以 exec 這樣的函數隻有失敗的返回值,失敗會返回 -1;沒有成功的返回值!所以一般而言,exec 的返回值不用判斷了,隻要往後走,就是出錯了!

3. 程序替換的接口

我們上面也看到了程序替換的接口一共有六個,分別如下:

我們上面寫的代碼都是用的 execl 這個接口,下面我們重新開始認識一下它。

(1) execl

我們可以看到 exec 系列的接口中,所有的接口都是以 exec 開頭的,其中 execl 後面的 l 代表什麼呢?其實 l 代表 list,傳參方式是以列表方式傳參。path 代表目標可執行程序的路徑和文件名;arg 代表如何執行,即命令行怎麼傳我們就怎麼傳,但是這個參數傳錯了也不會有影響,因為這個接口設計的時候防止我們傳參傳錯,會自動在路徑文件名中查找正確的指令。

(2) execlp

如上,execlp 這個接口隻是第一個參數和 execl 不同,那麼它的第一個參數代表什麼呢?首先,execlpexecl 多了一個字母 p,這個字母 p 代表 PATH,表示我們要找到的目標可執行程序,但是 execlp 會自動的去環境變量 PATH 中根據 file 去尋找可執行程序;所以它的第一個參數 file 代表我們需要執行的程序。我們也可以按照上面的代碼使用一下 execlp,如下代碼:

      1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <sys/wait.h>
      4 #include <sys/types.h>
      5 
      6 int main()
      7 {
      8     pid_t id = fork();
      9     if(id == 0)
     10     {
     11         // child
     12         printf("pid: %d, exec command begin\n", getpid());
     13         sleep(3);
     14         execlp("ls", "ls", "-a", "-l", NULL);
     15         printf("pid: %d, exec command end\n", getpid());
     16     }
     17     else
     18     {                                                                                                                             
     19         // father
     20         pid_t rid = waitpid(-1, NULL, 0);
     21         if(rid > 0)
     22         {
     23             printf("success wait, rid: %d\n", rid);
     24         }
     25     }
     26     return 0;
     27 }

運行結果如下:

(3) execv

如上圖,execv 為什麼叫 execv 呢?execv 後面的 v 我們可以理解成 vector,即一個數組,這個就要和第二個參數一起理解了;我們可以看到,第二個參數 argv 表示的是一個指針數組,其中 const 修飾的是指針指向的指向不能被修改,即第一個下標就隻能指向第一個元素,第二個下標就隻能指向第二個元素;這個指針數組就是指要將怎樣執行,選項等全部放入這個指針數組傳進去。我們也可以使用 execv 修改上面的代碼,如下:

       int main()
{
        char *const argv[] = {
          "ls",
          "-a",
          "-l",
          NULL
            };
            pid_t id = fork();
            if(id == 0)
            {
                // child                                                                                                                
                printf("pid: %d, exec command begin\n", getpid());
                sleep(3);
                execv("/usr/bin/ls", argv);
                printf("pid: %d, exec command end\n", getpid());
            }
            else 
            {
                // father
                pid_t rid = waitpid(-1, NULL, 0);
                if(rid > 0)
                {
                    printf("success wait, rid: %d\n", rid);
                }
            }
            return 0;
        }

執行結果如下:

(4) execvp

通過上面我們所學的知識,execvp 中的 vp 我們都應該知道是什麼意思了,v 可以理解成 vectorp 理解成 PATH;所以第一個參數就是傳我們需要執行的程序;argv 就是傳各種選項的指針數組。下面看代碼演示:

       int main()
{
        char *const argv[] = {
          "ls",
          "-a",
          "-l",
          NULL
            };
            pid_t id = fork();
            if(id == 0)
            {
                // child                                                                                                                
                printf("pid: %d, exec command begin\n", getpid());
                sleep(3);
                execvp(argv[0], argv);
                printf("pid: %d, exec command end\n", getpid());
            }
            else 
            {
                // father
                pid_t rid = waitpid(-1, NULL, 0);
                if(rid > 0)
                {
                    printf("success wait, rid: %d\n", rid);
                }
            }
            return 0;
        }

我們傳參可以直接傳 argv 的第一個元素,因為第一個元素就是我們需要的可執行程序;執行結果如下:

  • ps

學習了上面的四個接口,我們中途暫停一下,回顧一下我們的程序替換都是替換系統的程序,那麼我們可以替換自己寫的程序嗎?下面我們驗證一下,我們另外寫一個 c 的代碼:

      1 #include <iostream>
      2 
      3 int main()
      4 {
      5     std::cout << "test cpp" << std::endl;
      6     std::cout << "test cpp" << std::endl;
      7     std::cout << "test cpp" << std::endl;                                                                                         
      8     return 0;
      9 }

隨後我們使用 Makefile 生成可執行程序:

     testcpp:testcpp.cc
         g   -o $@ $^ -std=c  11
     mytest:mytest.c
         gcc -o $@ $^ -std=c99
     .PHONY:clean
     clean:
        rm -f mytest testcpp   

但是這樣我們隻能生成一套依賴關系,這裡有兩套依賴關系,那麼如何生成兩套依賴關系呢?可以像下面這樣處理:

      1 .PHONY:all
      2 all:testcpp mytest
      3 
      4 testcpp:testcpp.cc
      5     g   -o $@ $^ -std=c  11
      6 
      7 mytest:mytest.c
      8     gcc -o $@ $^ -std=c99
      9 .PHONY:clean
     10 clean:
     11     rm -f mytest testcpp    

我們 make 一下觀察:

如上就可以編譯兩個可執行程序了。現在我們要在 c語言 的程序中替換 c 的程序,所以我們在 c 文件中作以下修改:

       int main()
{
            pid_t id = fork();
            if(id == 0)
            {
                // child                                                                                                                
                printf("pid: %d, exec command begin\n", getpid());
                execl("./testcpp", "testcpp", NULL); 
                printf("pid: %d, exec command end\n", getpid());
            }
            else 
            {
                // father
                pid_t rid = waitpid(-1, NULL, 0);
                if(rid > 0)
                {
                    printf("success wait, rid: %d\n", rid);
                }
            }
            return 0;
        }

其中 execl("./testcpp", "testcpp", NULL); 中第一個參數 "./testcpp" 表示在當前路徑下找到可執行程序,所以第二個參數就可以不加 ./ 了;下面我們執行一下觀察結果:

如上,我們也可以替換我們自己寫的程序;另外,我們還可以替換自己寫的各種語言的程序,例如 pathonjava等都可以,因為這些程序運行起來都是進程,而 exec 這樣的接口都是進行進程程序替換的!

(5) execle

在學習 execle 這個接口之前,我們先回顧一下以前學的環境變量;我們知道,子進程的環境變量是通過父進程繼承下來的,因為環境變量存在於地址空間中,子進程會繼承父進程的地址空間;那麼父進程的環境變量是從哪裡來的呢?答案是從 bash 中來的,因為我們在命令行所運行的程序的父進程都是 bash,所以我們運行的程序的環境變量也就是繼承 bash 的!

下面我們介紹一個接口 putenv,它可以使當前的進程導入環境變量:

那麼我們下面開始驗證一下,首先我們使用 export 指令將一個環境變量導入到 bash 的環境變量表中,如下圖:

然後我們在 testcpp.cc 文件中打印環境變量表,然後在 mytest.c 中,用 testcpp.cc 替換子進程,觀察子進程是否繼承了父進程的環境變量表,如下圖:

如上圖,驗證了我們的思想是正確的。

隨後我們使用 putenv 在 mytest.c 這個父進程中導入環境變量,觀察進行替換程序後的子進程也是否繼承了父進程的環境變量表,如下代碼:

       int main()
{
            char* env_val = "MY_ENV2=222222222222222222222";
        putenv(env_val);   
            pid_t id = fork();
            if(id == 0)
            {
                // child                                                                                                                
                printf("pid: %d, exec command begin\n", getpid());
                execl("./testcpp", "testcpp", NULL); 
                printf("pid: %d, exec command end\n", getpid());
            }
            else 
            {
                // father
                pid_t rid = waitpid(-1, NULL, 0);
                if(rid > 0)
                {
                    printf("success wait, rid: %d\n", rid);
                }
            }
            return 0;
        }

運行後觀察結果,我們確實也發現了我們導入父進程的環境變量,如下圖:

所以我們得出一個結論:環境變量被子進程繼承下去是一種默認行為,不受程序替換的影響;因為通過地址空間可以讓子進程繼承父進程的環境變量數據,程序替換隻會替換新程序的代碼和數據,環境變量不會被替換!

所以如果我們想將父進程的環境變量原封不動傳給子進程可以用以上的方法;此外,還有另外一個方法,就是用 execle 這個接口,我們先看一下這個接口的介紹:

前兩個參數我們已經很熟悉了,其中最後一個參數 envp 就是我們需要傳的環境變量。我們將代碼改成下面的代碼:

       int main()
{
            pid_t id = fork();
            if(id == 0)
            {
                // child                                                                                                                
                printf("pid: %d, exec command begin\n", getpid());
                execle("./testcpp", "testcpp", "-a", "-b", NULL, environ); 
                printf("pid: %d, exec command end\n", getpid());
            }
            else 
            {
                // father
                pid_t rid = waitpid(-1, NULL, 0);
                if(rid > 0)
                {
                    printf("success wait, rid: %d\n", rid);
                }
            }
            return 0;
        }

運行之後結果:

如上圖,和第一種方式一樣,用 execle 接口我們也能看到子進程繼承了父進程的環境變量表。

如果我們想傳遞我們自己定義的環境變量呢?我們可以寫以下代碼進行傳遞:

     int main()
{
      char* const my_env[] = 
      {
        "MY_ENV1=111111111111111111111",
        "MY_ENV2=222222222222222222222",
        "MY_ENV3=333333333333333333333",
        NULL
      };
          pid_t id = fork();
          if(id == 0)
          {
              // child                                                                                                                
              printf("pid: %d, exec command begin\n", getpid());
              execle("./testcpp", "testcpp", "-a", "-b", NULL, my_env);  
              printf("pid: %d, exec command end\n", getpid());
          }
          else 
          {
              // father
              pid_t rid = waitpid(-1, NULL, 0);
              if(rid > 0)
              {
                  printf("success wait, rid: %d\n", rid);
              }
          }
          return 0;
      }

執行結果如下,就將我們自己定義的環境變量傳遞給子進程了:

同時,通過我們傳遞自己的環境變量表可以得出一個結論:在使用 execle 接口時,環境變量的參數並不是以新增的形式傳遞給子進程,而是覆蓋式傳遞

那麼我們想要新增呢?其實我們上面已經做過了,就是使用 putenv 的接口新增之後,傳遞給子進程!

所以通過上面,我們得出結論:程序替換可以將命令行參數和環境變量通過自己的參數,傳遞給被替換的程序的 main 函數中!

(6) execvpe

我們先看一下它的文檔介紹:


通過上面的學習,我們已經知道
vpe 分別代表什麼了,所以我們使用起來就不是問題了,這裡就不作多介紹了。

(7) execve

通過手冊發現,我們上面使用的6個接口都是 3號手冊,即都是庫函數的接口,其實它們都是通過一個系統調用 execve 封裝過的,而這個 execve 所在的手冊是 2號手冊,即系統調用,如下圖:

那麼為什麼要對這6個接口進行封裝呢?原因是因為主要還是為了滿足各種調用的場景。

五、簡單實現shell

現在我們利用當前所學的知識簡單模擬實現一個我們自己的 shell 命令行,代碼如下:

        1 #include <stdio.h>
        2 #include <unistd.h>
        3 #include <string.h>
        4 #include <stdlib.h>
        5 #include <sys/types.h>
        6 #include <sys/wait.h>
        7 
        8 #define SEP " "
        9 #define SIZE 64
       10 #define NUM 1024
       11 
       12 int lastcode = 0;
       13 char cwd[1024];
       14 
       15 const char* getUserName()
       16 {
       17     const char* name = getenv("USER");
       18     if(name) return name;
       19     else return "none";
       20 }
       21 
       22 const char* getHostName()
       23 {
       24     const char* hostname = getenv("HOSTNAME");
       25     if(hostname) return hostname;
       26     else return "none";
       27 }
       28 
       29 const char* getCwd()
       30 {                                                                                                                                           
       31     const char* cwd = getenv("PWD");
       32     if(cwd) return cwd;
       33     else return "none";
       34 }
       35 
       36 
       37 int getUserCommand(char* command, int num)
       38 {
       39     printf("[%s@%s %s]# ", getUserName(), getHostName(), getCwd());                                                                         
       40     char* input = fgets(command, num, stdin);   // 最後輸入的是 \n
       41     if(input == NULL) return -1;
       42 
       43     command[strlen(command) - 1] = '\0';
       44     return strlen(command);
       45 }
       46 
       47 
       48 void CommandSplit(char* in, char* out[])
       49 {
       50     int argc = 0;
       51     out[argc  ] = strtok(in, SEP);
       52 
       53     // 截取以空格為分隔符的字符串放入out中,因為 in 字符最後以NULL結尾,所以當沒截取到NULL時循環繼續
       54     // 其中 strtok 為截取字符串的庫函數,第一個參數為需要截取的字符串,當設為NULL時,會繼續掃描上一次成功調用函數的位置
    W> 55     while(out[argc  ] = strtok(NULL, SEP));
       56 }
       57 
       58 
       59 int excute(char* argv[])
       60 {
       61     pid_t id = fork();
       62     if(id < 0) return -1;
       63     else if(id == 0)
       64     {
       65         // child
       66         // exec command 
       67         execvp(argv[0], argv);
       68         exit(1);
       69     }                                                                                                                                       
       70     else
       71     {
       72         // father
       73         int status = 0;
       74         pid_t rid = waitpid(id, &status, 0);
       75 
       76         // 獲取退出碼
       77         if(rid > 0)
       78         {
       79             lastcode = WEXITSTATUS(status);
       80         }
       81     }
       82     return 0;
       83 }
       84 
       85 void cd(const char* path)
       86 {
       87     // chdir---更改工作路徑的接口,誰調就更改誰的工作路徑
       88     chdir(path);
       89     char tmp[1024];
       90     // 獲取進程當前所在的絕對工作路徑
       91     getcwd(tmp, sizeof tmp);
       92     sprintf(cwd, "PWD=%s", tmp);
       93     putenv(cwd);
       94 }
       95 
       96 // 重新理解內建命令,它其實就是 bash 自己執行的,類似與自己內部的一個函數!
       97 // 返回1代表是內建命令,0表示不是內建命令 
       98 int isBuildin(char* argv[])
       99 {
      100     if(strcmp("cd", argv[0]) == 0)
      101     {
      102         char* path = NULL;                                                                                                                  
    W>103         if(argv[1] == NULL) path = ".";
      104         else path = argv[1];
      105         cd(path);
      106         return 1;
      107     }
      108     else if(strcmp("echo", argv[0]) == 0)
      109     {
      110         if(argv[1] == NULL)
      111         {
      112             printf("\n");
      113             return 1;
      114         }
      115         if(*(argv[1]) == '$' && strlen(argv[1]) > 1)
      116         {    
      117             char* val = argv[1]   1; 
      118             if(strcmp(val, "?") == 0)
      119             {
      120                 printf("%d\n", lastcode);
      121                 lastcode = 0;
      122             }
      123             else 
      124             {
      125                 const char* enval = getenv(val);
      126                 if(enval) printf("%s\n", enval);
      127                 else printf("\n");
      128             }
      129             return 1;
      130         }
      131 
      132         else 
      133         {
      134             printf("%s\n", argv[1]);
      135             return 1;                                                                                                                       
      136         }
      137         
      138         return 0;
      139     }
      140 
      141     // else if(0) {}
      142     // ...
      143 
      144     return 0;
      145 }
      146 
      147 int main()
      148 {
      149     char usercommand[NUM];
      150     char* argv[SIZE];
      151 
      152     while(1)
      153     {
      154         // 1、打印提示符並且獲取用戶命令字符串長度,如果小於等於0,沒有意義,繼續重新獲取
      155         int n = getUserCommand(usercommand, sizeof usercommand);    
      156         if(n <= 0) continue;   
      157 
      158         // 2、分割字符串 --- "ls -a -l" -> "ls" "-a" "-l"
      159         CommandSplit(usercommand, argv);
      160 
      161         // 3、判斷並執行內建命令
      162         n = isBuildin(argv);
      163         if(n) continue;
      164 
      165         // 4、執行對應的命令
      166         excute(argv);
      167     }
      168 
      169     return 0;
      170 }