linux c語言 fork() 和 exec 函數的簡介和用法


linux c語言 fork() 和 exec 函數的簡介和用法

 

      假如我們在編寫1個c程序時想調用1個shell腳本或者執行1段 bash shell命令, 應該如何實現呢?

      其實在<stdlib.h> 這個頭文件中包含了1個調用shell命令或者腳本的函數 system();直接把 shell命令作為參數傳入 system函數就可以了, 的確很方便. 關於system 有一段這樣的介紹:   system 執行時內部會自動啟用fork() 新建1個進程,  效率沒有直接使用fork() 和 exec函數高.

 

       那么這篇文章其實就是介紹一下fork() 和 exec函數的用法, 以及如何使用它們來替代system函數.

      

1. fork() 函數

1.1 fork() 函數的作用

       一般來講, 我們編寫1個普通的c程序, 運行這個程序直到程序結束, 系統只會分配1個pid給這個程序, 也就就說, 系統里只會有一條關於這個程序的進程.

 

        但是執行了fork() 這個函數就不同了. 

        fork 這個英文單詞在英文里是"分叉"意思,  fork() 這個函數作用也很符合這個意思.  它的作用是復制當前進程(包括進程在內存里的堆棧數據)為1個新的鏡像. 然后這個新的鏡像和舊的進程同時執行下去. 相當於本來1個進程, 遇到fork() 函數后就分叉成兩個進程同時執行了. 而且這兩個進程是互不影響

 

        參考下面這個小程序:

 

[cpp]  view plain  copy
 
  1. int fork_3(){  
  2.     printf("it's the main process step 1!!\n\n");  
  3.   
  4.     fork();  
  5.   
  6.     printf("step2 after fork() !!\n\n");  
  7.   
  8.     int i; scanf("%d",&i);   //prevent exiting  
  9.     return 0;  
  10. }  


 

          在這個函數里, 共有兩條printf語句, 但是執行執行時則打出了3行信息. 如下圖: 

 

            為什么呢, 因為fork()函數將這個程序分叉了啊,  見下面的圖解:

 

         可以見到程序在fork()函數執行時都只有1條主進程, 所以 step 1 會被打印輸出1次.

         執行 fork()函數后,  程序分叉成為了兩個進程, 1個是原來的主進程,  另1個是新的子進程, 它們都會執行fork() 函數后面的代碼, 所以 step2 會被 兩條進程分別打印輸出各一次, 屏幕上就總共3條printf 語句了!

 

         可以見到這個函數最后面我用了 scanf()函數來防止程序退出,  這時查看系統的進程, 就會發現兩個相同名字的進程:

 

 

如上圖, pid 8808 那個就是主進程了, 而 pid  8809那個就是子進程啊, 因為它的parent pid是 8808啊!

          

          需要注意的是, 假如沒有做特殊處理, 子進程會一直存在, 即使fork_3()函數被調用完成,  子進程會和主程序一樣,返回調用fork_3() 函數的上一級函數繼續執行, 直到整個程序退出.

 

          可以看出, 假如fork_3() 被執行2次,  主程序就會分叉兩次, 最終變成4個進程, 是不是有點危險. 所以上面所謂的特殊處理很重要啊!

 

1.2 區別分主程序和子程序.

        實際應用中, 單純讓程序分叉意義不大, 我們新增一個子程序, 很可能是為了讓子進程單獨執行一段代碼. 實現與主進程不同的功能.

         要實現上面所說的功能, 實際上就是讓子進程和主進程執行不同的代碼啊.

         所以fork() 實際上有返回值, 而且在兩條進程中的返回值是不同的, 在主進程里 fork()函數會返回主進程的pid,   而在子進程里會返回0!   所以我們可以根據fork() 的返回值來判斷進程到底是哪個進程, 就可以利用if 語句來執行不同的代碼了!

 

        如下面這個小程序fork_1():

 

[cpp]  view plain  copy
 
  1. int fork_1(){  
  2.     int childpid;  
  3.     int i;  
  4.   
  5.     if (fork() == 0){  
  6.         //child process  
  7.         for (i=1; i<=8; i++){  
  8.             printf("This is child process\n");  
  9.         }  
  10.     }else{  
  11.         //parent process  
  12.         for(i=1; i<=8; i++){  
  13.             printf("This is parent process\n");  
  14.         }  
  15.     }  
  16.   
  17.     printf("step2 after fork() !!\n\n");  
  18. }  

        我對fork() 函數的返回值進行了判斷, 如果 返回值是0, 我就讓認為它是子進程, 否則是主程序.  那么我就可以讓這兩條進程輸出不同的信息了.

 

       

          輸出信息如下圖:

 

          可以見到 子程序和主程序分別輸出了8條不同的信息,  但是它們並不是規則交替輸出的, 因為它們兩條進程是互相平行影響的, 誰的手快就在屏幕上先輸出,  每次運行的結果都有可能不同哦.

 

        下面是圖解:

 

          由圖解知兩條進程都對fork()返回值執行判斷,  在if 判斷語句中分別執行各自的代碼.  但是if判斷完成后,  還是會回各自執行接下來的代碼. 所以 step2 還是輸出了2次.

1.4 使用exit() 函數令子進程在if 判斷內結束.

          參考上面的函數, 雖然使用if 對 fork() 的返回值進行判斷,  實現了子進程和 主進程在if判斷的范圍內執行了不同的代碼,  但是就如上面的流程圖, 一旦if執行完成, 他們還是會各自執行后面的代碼. 

          通常這不是我們期望的,  我們更多時會希望子進程執行一段特別的代碼后就讓他結束,  后面的代碼讓主程序執行就行了.

          這個實現起來很簡單, 在子程序的if 條件內最后加上exit() 函數就ok了.

 

         將上面的fork_1()函數修改一下, 加上exit語句:

 

[cpp]  view plain  copy
 
  1. int fork_1(){  
  2.     int childpid;  
  3.     int i;  
  4.   
  5.     if (fork() == 0){  
  6.         //child process  
  7.         for (i=1; i<=8; i++){  
  8.             printf("This is child process\n");  
  9.         }  
  10.         exit(0);  
  11.     }else{  
  12.         //parent process  
  13.         for(i=1; i<=8; i++){  
  14.             printf("This is parent process\n");  
  15.         }  
  16.     }  
  17.   
  18.     printf("step2 after fork() !!\n\n");  
  19. }  

       再看看輸出:

 

 


            可以見到, step2只輸出1次了,   這是因為子程序在 if條件內結束了啊, 一旦 if 判斷成, 就只剩下1個主進程執行下面的代碼了, 這正是我們想要的!

            注意: exit() 函數在 stdlib.h 頭文件內

 

流程圖:

 

 

 

1.4 使用wait() 函數主程序等子程序執行完成(退出)后再執行.   

 

        由上面例子得知,  主程序和子程序的執行次序是隨機的,  但是實際情況下, 通常我們希望子進程執行后,  才繼續執行主進程. 

        例如對於上面的fork_1()函數, 我想先輸出子進程的8個 "This is child process"  然后再輸出 8個 主進程"This is parent process", 改如何做?

        wait()函數就提供了這個功能,    在if 條件內的  主進程呢部分內 加上wait() 函數, 就可以讓主進程執行fork()函數時先hold 住, 等子進程退出后再執行, 通常會配合子進程的exit()函數一同使用.

 

        我將fork_1()函數修改一下, 添加了wait()語句:

 

[cpp]  view plain  copy
 
  1. int fork_1(){  
  2.     int childpid;  
  3.     int i;  
  4.   
  5.     if (fork() == 0){  
  6.         //child process  
  7.         for (i=1; i<=8; i++){  
  8.             printf("This is child process\n");  
  9.         }  
  10.         exit(0);  
  11.     }else{  
  12.         //parent process  
  13.         wait();  
  14.         for(i=1; i<=8; i++){  
  15.             printf("This is parent process\n");  
  16.         }  
  17.     }  
  18.   
  19.     printf("step2 after fork() !!\n\n");  
  20. }  

 

輸出:

 

      見到這時的屏幕輸出就很有規律了!

      其實wait() 函數還有1個功能, 就是可以接收1個 pid_t(在unistd.h內,其實就是Int啦) 指針類型參數,   給這個參數賦上子進程退出前的系統pid值

     流程圖:

  

 

 

 

 

2. exec 函數組

 

      需要注意的是exec並不是1個函數, 其實它只是一組函數的統稱, 它包括下面6個函數:

     

[cpp]  view plain  copy
 
  1. #include <unistd.h>  
  2.   
  3. int execl(const char *path, const char *arg, ...);  
  4.   
  5. int execlp(const char *file, const char *arg, ...);  
  6.   
  7. int execle(const char *path, const char *arg, ..., char *const envp[]);  
  8.   
  9. int execv(const char *path, char *const argv[]);  
  10.   
  11. int execvp(const char *file, char *const argv[]);  
  12.   
  13. int execve(const char *path, char *const argv[], char *const envp[]);  

 

       可以見到這6個函數名字不同, 而且他們用於接受的參數也不同.

       實際上他們的功能都是差不多的, 因為要用於接受不同的參數所以要用不同的名字區分它們, 畢竟c語言沒有函數重載的功能嘛..  

 

       但是實際上它們的命名是有規律的:

       exec[l or v][p][e]

       exec函數里的參數可以分成3個部分,      執行文件部分,     命令參數部分,   環境變量部分.

        例如我要執行1個命令   ls -l /home/gateman  

        執行文件部分就是  "/usr/bin/ls"

        命令參賽部分就是 "ls","-l","/home/gateman",NULL              見到是以ls開頭 每1個空格都必須分開成2個部分, 而且以NULL結尾的啊.

        環境變量部分, 這是1個數組,最后的元素必須是NULL 例如  char * env[] = {"PATH=/home/gateman", "USER=lei", "STATUS=testing", NULL};

        

        好了說下命名規則:

        e后續,  參數必須帶環境變量部分,   環境變零部分參數會成為執行exec函數期間的環境變量, 比較少用

        l 后續,   命令參數部分必須以"," 相隔, 最后1個命令參數必須是NULL

        v 后續,   命令參數部分必須是1個以NULL結尾的字符串指針數組的頭部指針.         例如char * pstr就是1個字符串的指針, char * pstr[] 就是數組了, 分別指向各個字符串.

        p后續,   執行文件部分可以不帶路徑, exec函數會在$PATH中找

 

          

         還有1個注意的是, exec函數會取代執行它的進程,  也就是說, 一旦exec函數執行成功, 它就不會返回了, 進程結束.   但是如果exec函數執行失敗, 它會返回失敗的信息,  而且進程繼續執行后面的代碼!

 

       通常exec會放在fork() 函數的子進程部分, 來替代子進程執行啦, 執行成功后子程序就會消失,  但是執行失敗的話, 必須用exit()函數來讓子進程退出!

       下面是各個例子:

 

2.1  execv 函數

 

[cpp]  view plain  copy
 
  1. int childpid;  
  2. int i;  
  3.   
  4. if (fork() == 0){  
  5.     //child process  
  6.     char * execv_str[] = {"echo", "executed by execv",NULL};  
  7.     if (execv("/usr/bin/echo",execv_str) <0 ){  
  8.         perror("error on exec");  
  9.         exit(0);  
  10.     }  
  11. }else{  
  12.     //parent process  
  13.     wait(&childpid);  
  14.     printf("execv done\n\n");  
  15. }  
注意字符串指針數組的定義和賦值

 

 

2.2  execvp 函數

 

 

[cpp]  view plain  copy
 
  1. if (fork() == 0){  
  2.     //child process  
  3.     char * execvp_str[] = {"echo", "executed by execvp",">>", "~/abc.txt",NULL};  
  4.     if (execvp("echo",execvp_str) <0 ){  
  5.         perror("error on exec");  
  6.         exit(0);  
  7.     }  
  8. }else{  
  9.     //parent process  
  10.     wait(&childpid);  
  11.     printf("execvp done\n\n");  
  12. }  

 

2.3 execve 函數

 

[cpp]  view plain  copy
 
  1. if (fork() == 0){  
  2.     //child process  
  3.     char * execve_str[] = {"env",NULL};  
  4.     char * env[] = {"PATH=/tmp", "USER=lei", "STATUS=testing", NULL};  
  5.     if (execve("/usr/bin/env",execve_str,env) <0 ){  
  6.         perror("error on exec");  
  7.         exit(0);  
  8.     }  
  9. }else{  
  10.     //parent process  
  11.     wait(&childpid);  
  12.     printf("execve done\n\n");  
  13. }  

 

 

2.4 execl 函數

 

[cpp]  view plain  copy
 
  1. if (fork() == 0){  
  2.     //child process  
  3.     if (execl("/usr/bin/echo","echo","executed by execl" ,NULL) <0 ){  
  4.         perror("error on exec");  
  5.         exit(0);  
  6.     }  
  7. }else{  
  8.     //parent process  
  9.     wait(&childpid);  
  10.     printf("execv done\n\n");  
  11. }  

 

2.5 execlp 函數

 

[cpp]  view plain  copy
 
  1. if (fork() == 0){  
  2.     //child process  
  3.     if (execlp("echo","echo","executed by execlp" ,NULL) <0 ){  
  4.         perror("error on exec");  
  5.         exit(0);  
  6.     }  
  7. }else{  
  8.     //parent process  
  9.     wait(&childpid);  
  10.     printf("execlp done\n\n");  
  11. }  


 

2.6 execle 函數

 

[cpp]  view plain  copy
 
  1. if (fork() == 0){  
  2.     //child process  
  3.     char * env[] = {"PATH=/home/gateman", "USER=lei", "STATUS=testing", NULL};  
  4.     if (execle("/usr/bin/env","env",NULL,env) <0){  
  5.         perror("error on exec");  
  6.         exit(0);  
  7.     }  
  8. }else{  
  9.     //parent process  
  10.     wait(&childpid);  
  11.     printf("execle done\n\n");  
  12. }  


 

 

 輸出:


 

 

3. fork() 和exec 函數與system()函數比較

     見到上面execvp函數的輸出. 你會發現 exec函數只是系統調用, 它是不支持管線處理的

     而system()函數是支持的.   他的內部會自動fork() 1個子進程,但是效率沒有fork() 和 exec配合使用好.

 

     但是exec 支持執行腳本.  所以不需要管線處理的命令或者腳本可以利用fork() 和 exec函數來執行.

 

 

4. 利用 fwrite() ,fork() 和exec 函數 替代system()函數.

 

     上面講過了, 雖然exec函數不支持管線, 而且命令參數復雜, 但是它支持執行腳本啊, 所以我們可以使用fwrite將 有管線處理的命令寫入1個腳本中, 然后利用exec函數來執行這個腳本.

     下面會編寫1個base_exec(char *) 函數, 接收1個字符串參數,   然后執行它.

 

      這里只會大概寫出這個函數的邏輯步驟:

      1. 利用getuid函數獲得當前的pid,  然后利用pid獲得當前唯一的文件名, 避免因為相同程序同時執行發生沖突!

      2.  利用fwrite函數在 /tmp/下面  建立1個上面文件名的腳本文件.     因為/tmp/ 任何用戶都可以讀寫啊

     3.  把命令參數寫入腳本

     4. 利用fork() 和 exec() 執行這個腳本

     5. 有需要的話當exec執行完, 記錄日志.

 

     下面就是i代碼:

頭文件:

base_exec.h

 

[cpp]  view plain  copy
 
  1. #ifndef __BASE_EXEC_H_  
  2. #define __BASE_EXEC_H_  
  3.   
  4.     int base_exec(char *) ;  
  5.   
  6. #endif /* BASE_EXEC_H_ */  

源文件:

 

base_exec.c

 

[cpp]  view plain  copy
 
  1. #include "base_exec.h"  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4. #include <string.h>  
  5. #include <unistd.h>  
  6. #include <time.h>  
  7.   
  8. #define LOGFILE "/home/gateman/logs/c_exec.log"  
  9.   
  10. int base_exec(char * pcmd){  
  11.     FILE * pf;  
  12.     pid_t pid = getpid();  
  13.     char pfilename[20];  
  14.     sprintf(pfilename, "/tmp/base_exec%d.sh",pid);  
  15.   
  16.     pf=fopen(pfilename,"w"); //w is overwrite, a is add  
  17.     if (NULL == pf){  
  18.         printf("fail to open the file base_exec.sh!!!\n");  
  19.         return -1;  
  20.     }  
  21.   
  22.     fwrite("#!/bin/bash\n", 12, 1, pf);  
  23.     fwrite(pcmd, strlen(pcmd),1, pf);  
  24.     fwrite("\n", 1,1, pf);  
  25.   
  26.     fclose(pf);  
  27.   
  28.     if (fork() ==0 ){  
  29.         //child processj  
  30.         char * execv_str[] = {"bash", pfilename, NULL};  
  31.         if (execv("/bin/bash",execv_str) < 0){  
  32.             perror("fail to execv");  
  33.             exit(-1);  
  34.         }  
  35.     }else{  
  36.         //current process  
  37.         wait();  
  38.         pf=fopen(LOGFILE,"a");  
  39.   
  40.         if (NULL == pf){  
  41.             printf("fail to open the logfile !!!\n");  
  42.             return -1;  
  43.         }  
  44.         time_t t;  
  45.         struct tm * ptm;  
  46.         time(&t);  
  47.         ptm  = gmtime(&t);  
  48.         char cstr[24];  
  49.         sprintf (cstr, "time: %4d-%02d-%02d %02d:%02d:%02d\n", 1900+ptm->tm_year,ptm->tm_mon,ptm->tm_mday,ptm->tm_hour,ptm->tm_min,ptm->tm_sec);  
  50.         fwrite(cstr, strlen(cstr),1, pf);  
  51.   
  52.         int uid = getuid();  
  53.         sprintf(cstr, "uid: %d\ncommand:\n",uid);  
  54.         fwrite(cstr, strlen(cstr),1, pf);  
  55.   
  56.         fwrite(pcmd, strlen(pcmd),1, pf);  
  57.         fwrite("\n\n\n", 3,1, pf);  
  58.         fclose(pf);  
  59.         remove(pfilename);  
  60.         return 0;  
  61.     }  
  62.     return 0;  
  63. }  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM