fork+exec 與system,popen區別


1、fork + exec

fork用來創建一個子進程。一個程序一調用fork函數,系統就為一個新的進程准備了前述三個段,首先,系統讓新的進程與舊的進程使用同一個代碼段,因為它們的程序還是相同的,對於數據段和堆棧段,系統則復制一份給新的進程,這樣,父進程的所有數據都可以留給子進程,但是,子進程一旦開始運行,雖然它繼承了父進程的一切數據,但實際上數據卻已經分開,相互之間不再有影響了,也就是說,它們之間不再共享任何數據了。而如果兩個進程要共享什么數據的話,就要使用另一套函數(shmget,shmat,shmdt等)來操作。現在,已經是兩個進程了,對於父進程,fork函數返回了子程序的進程號,而對於子程序,fork函數則返回零,這樣,對於程序,只要判斷fork函數的返回值,就知道自己是處於父進程還是子進程中。

事實上,目前大多數的unix系統在實現上並沒有作真正的copy。一般的,CPU都是以“頁”為單位分配空間的,象INTEL的CPU,其一頁在通常情況下是4K字節大小,而無論是數據段還是堆棧段都是由許多“頁”構成的,fork函數復制這兩個段,只是“邏輯”上的,並非“物理”上的,也就是說,實際執行fork時,物理空間上兩個進程的數據段和堆棧段都還是共享着的,當有一個進程寫了某個數據時,這時兩個進程之間的數據才有了區別,系統就將有區別的“頁”從物理上也分開。系統在空間上的開銷就可以達到最小。

vfork和fork一樣,也是創建一個子進程,但是它並不將父進程的地址空間完全復制到子進程中,不會復制頁表。因為子進程會立即調用exec,於是也就不會存放該地址空間。不過在子進程中調用exec或exit之前,他在父進程的空間中運行。

為什么會有vfork,因為以前的fork當它創建一個子進程時,將會創建一個新的地址空間,並且拷貝父進程的資源,而往往在子進程中會執行exec調用,這樣,前面的拷貝工作就是白費力氣了,這種情況下,聰明的人就想出了vfork,它產生的子進程剛開始暫時與父進程共享地址空間(其實就是線程的概念了),因為這時候子進程在父進程的地址空間中運行,所以子進程不能進行寫操作,並且在兒子“霸占”着老子的房子時候,要委屈老子一下了,讓他在外面歇着(阻塞),一旦兒子執行了exec或者exit后,相當於兒子買了自己的房子了,這時候就相當於分家了。

vfork和fork之間的另一個區別是: vfork保證子進程先運行,在她調用exec或exit之后父進程才可能被調度運行。如果在調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。

由此可見,這個系統調用是用來啟動一個新的應用程序。其次,子進程在vfork()返回后直接運行在父進程的棧空間,並使用父進程的內存和數據。這意味着子進程可能破壞父進程的數據結構或棧,造成失敗。

為了避免這些問題,需要確保一旦調用vfork(),子進程就不從當前的棧框架中返回,並且如果子進程改變了父進程的數據結構就不能調用exit函數。子進程還必須避免改變全局數據結構或全局變量中的任何信息,因為這些改變都有可能使父進程不能繼續。

通常,如果應用程序不是在fork()之后立即調用exec(),就有必要在fork()被替換成vfork()之前做仔細的檢查。

用fork函數創建子進程后,子進程往往要調用一種exec函數以執行另一個程序,當進程調用一種exec函數時,該進程完全由新程序代換,而新程序則從其main函數開始執行,因為調用exec並不創建新進程,所以前后的進程id 並未改變,exec只是用另一個新程序替換了當前進程的正文,數據,堆和棧段。

一個進程一旦調用exec類函數,它本身就“死亡”了,系統把代碼段替換成新的程序的代碼,廢棄原有的數據段和堆棧段,並為新程序分配新的數據段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程序了。不過exec類函數中有的還允許繼承環境變量之類的信息,這個通過exec系列函數中的一部分函數的參數可以得到。

 

2、system

system 可以看做是fork + execl + waitpid。system()函數功能強大,很多人用卻對它的原理知之甚少先看linux版system函數的源碼:


#include 
#include 
#include 
#include

int system(const char * cmdstring)

{
    pid_t pid;
    int status;

    if(cmdstring == NULL){
          
         return (1);
    }


    if((pid = fork())<0){

            status = -1;
    }


    else if(pid = 0){

        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);

        -exit(127); //子進程正常執行則不會執行此語句

        }

    else{

            while(waitpid(pid, &status, 0) < 0){

                if(errno != EINTER){

                    status = -1;

                    break;

                }

            }

        }

        return status;

}


先分析一下原理,然后再看上面的代碼大家估計就能看懂了:  
    當system接受的命令為NULL時直接返回,否則fork出一個子進程,因為fork在兩個進程:父進程和子進程中都返回,這里要檢查返回的pid,fork在子進程中返回0,在父進程中返回子進程的pid,父進程使用waitpid等待子進程結束,子進程則是調用execl來啟動一個程序代替自己,execl("/bin/sh", "sh", "-c", cmdstring, (char*)0)是調用shell,這個shell的路徑是/bin/sh,后面的字符串都是參數,然后子進程就變成了一個shell進程,這個shell的參數是cmdstring,就是system接受的參數。在windows中的shell是command,想必大家很熟悉shell接受命令之后做的事了。
  
如果上面的你沒有看懂,那我再解釋下fork的原理:當一個進程A調用fork時,系統內核創建一個新的進程B,並將A的內存映像復制到B的進程空間中,因為A和B是一樣的,那么他們怎么知道自己是父進程還是子進程呢,看fork的返回值就知道,上面也說了fork在子進程中返回0,在父進程中返回子進程的pid。

    windows中的情況也類似,就是execl換了個又臭又長的名字,參數名也換的看了讓人發暈的,我在MSDN中找到了原型,給大家看看:

HINSTANCE   ShellExecute(
               HWND   hwnd,
               LPCTSTR   lpVerb,
               LPCTSTR   lpFile,
               LPCTSTR   lpParameters,
               LPCTSTR   lpDirectory,
               INT   nShowCmd
   );  

      用法見下:

     ShellExecute(NULL,   "open",   "c://a.reg",   NULL,   NULL,   SW_SHOWNORMAL);  


    你也許會奇怪 ShellExecute中有個用來傳遞父進程環境變量的參數 lpDirectory,linux中的 execl卻沒有,這是因為execl是編譯器的函數(在一定程度上隱藏具體系統實現),在linux中它會接着產生一個linux系統的調用 execve, 原型見下:
    int execve(const char * file,const char **argv,const char **envp);
  
    看到這里你就會明白為什么system()會接受父進程的環境變量,但是用system改變環境變量后,system一返回主函數還是沒變。原因從system的實現可以看到,它是通過產生新進程實現的,從我的分析中可以看到父進程和子進程間沒有進程通信,子進程自然改變不了父進程的環境變量。

 

 關於返回值,如果system()在調用/bin/sh時失敗則返回127,其他失敗原因返回-1。如果返回值為0,表示調用成功但是沒有出現子進程。若參數string為空指針(NULL),則返回非零值。如果system()調用成功則最后會返回執行shell命令后的返回值,但是此返回值也有可能為 system()調用/bin/sh失敗所返回的127,因此最好能再檢查errno 來確認執行成功。
 
 shell命令的返回值可以通過WEXITSTATUS(stat)得到。處理返回值的宏定義在 中,包括(stat是waitpid()的返回值):
         WIFEXITED(stat)Non zero if child exited normally.
         非零 如果子程序正常退出
         WEXITSTATUS(stat)exit code returned by child
         子程序返回exit 值
         WIFSIGNALED(stat)Non-zero if child was terminated by a signal
         非零 如果子進程被一個信號結束
         WTERMSIG(stat)signal number that terminated child
         結束子進程的signal number
         WIFSTOPPED(stat)non-zero if child is stopped
         非零 如果子進程被停止
         WSTOPSIG(stat)number of signal that stopped child
         停止子進程的signal number.
         WIFCONTINUED(stat)non-zero if status was for continued child
         非零 如果狀態是繼續運行的子進程
         WCOREDUMP(stat)If WIFSIGNALED(stat) is non-zero, this is non-zero if the process leftbehind a core dump.
         如果WIFSIGNALED(stat)非零,而且進程產生了一個core dump,那么這個也是非零。

 

3、popen

popen()也常常被用來執行一個程序。

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

popen() 函數用創建管道的方式啟動一個 進程, 並調用 shell. 因為管道是被定義成單向的, 所以 type 參數只能定義成只讀或者只寫, 不能是兩者同時, 結果流也相應的是只讀或者只寫. command 參數是一個字符串指針, 指向的是一個以 null 結束符結尾的字符串, 這個字符串包含一個 shell 命令. 這個命令被送到 /bin/sh 以 -c 參數執行, 即由 shell 來執行. type 參數也是一個指向以 null 結束符結尾的字符串的指針, 這個字符串必須是 'r' 或者 'w’ 來指明是讀還是寫.

popen() 函數的返回值是一個普通的標准I/O流, 它只能用 pclose() 函數來關閉, 而不是 fclose() 函數. 向這個流的寫入被轉化為對 command 命令的標准輸入; 而 command 命令的標准輸出則是和調用 popen(), 函數的進程相同,除非這個被command命令自己改變. 相反的, 讀取一個 “被popen了的” 流, 就相當於讀取 command 命令的標准輸出, 而 command 的標准輸入則是和調用 popen, 函數的進程相同.

注意, popen 函數的 輸出流默認是被全緩沖的.

pclose 函數等待相關的進程結束並返回一個 command 命令的退出狀態, 就像 wait4 函數 一樣

#include

int main(int argc, char *argv[])
{
         char buf[128];
         FILE *pp;

         if( (pp = popen("ls -l", "r")) == NULL )
         {
                 printf("popen() error!/n");
                 exit(1);
         }

         while(fgets(buf, sizeof buf, pp))
         {
                 printf("%s", buf);
         }
         pclose(pp);
         return 0;
}


免責聲明!

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



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