Linux進程的基本操作:fork vfork exec

進程創建

進入進程的運行狀態時,需要首先創建一個新的進程。在Linux系統中,提供了幾個關於創建新進程的操作函數,如fork()函數、vfork()函數和exec()函數族等。

1.fork()函數

fork()函數的功能是創建一個新的進程,新進程爲當前進程的子進程,那麼當前的進程就被稱爲父進程。在一個函數中,可以通過fork()函數的返回值判斷進程是在子進程中還是父進程中。fork()函數的調用形式爲:

pid_t fork(void);

使用fork()函數需要引用 sys/types.h 和 unistd.h頭文件,該函數的返回值類型爲pid_t,表示一個非負整數。若程序運行在父進程中,函數返回的PID爲子進程的進程號;若運行在子進程中,返回的PID爲0.
如若調用fork()函數創建子進程失敗,那麼就會返回-1,並且提示錯誤信息。錯誤信息有以下兩種形式:
EAGAIN:表示fork()函數沒有足夠的內存用於複製父進程的分頁表和進程結構數據。
ENOMEM:表示fork()函數分配必要的內核數據結構時內存不足。

#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(void)
{
    pid_t pid;
    if((pid = fork()) < 0)
    {
        printf("fork error!\n");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("in the child process!\n");
    }
    else
    {
        printf("in the parrent process!\n");
    }
    exit(0);
}

在實例中,通過fork()函數的返回值確定程序是運行在父進程中還是子進程中。運行結果如下

gcc -g -o fork1 fork1.c
./fork1
in the parrent process!
in the child process!

由程序的結果可以發現fork()函數的一個特點,那就是“調用一次,返回兩次”,這樣的特點是如何實現的呢?通過下圖可以分析其原因。
這裏寫圖片描述
從上圖可以看出,在一個程序中,調用到fork()函數後,就出現了分叉。在子進程中,fork()函數返回0;在父進程中,fork()函數返回子進程的ID。因此,fork()函數返回值後,開發人員可以根據返回值的不同,對父進程和子進程執行不同的代碼,這樣就使得fork()函數具有“調用一次,返回兩次”的特點。但是,父進程與子進程的返回順序並不是固定的,由於fork()函數是系統調用函數,因此取決於系統中其他進程的運行情況和內核的調度算法。

2.vfork()函數

vfork()函數與fork()函數相同,都是系統調用函數,兩者的區別是在創建子進程時fork()函數會複製所有父進程的資源,包括進程環境、內存資源等,而vfork()函數在創建子進程時不會複製父進程的所有資源,父子進程共享地址空間。這樣,在子進程中對虛擬內存空間中變量的修改,實際上是在修改父進程虛擬內存空間中的值。

#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int gvar = 2;
int main(void)
{
    pid_t pid;
    int var = 5;
    printf("process id:%ld\n",(long)getpid());
    printf("gvar = %d var = %d\n",gvar,var);
    if((pid = vfork()) < 0)
    {
        printf("error!\n");
        return 1;
    }
    else if(pid == 0)
    {
        gvar--;
        var++;
        printf("the child process id:%ld\ngvar = %d var = %d\n",(long)getpid(),gvar,var);
        _exit(0);
    }
    else
    {
        printf("the parrent process id:%ld\ngvar = %d var = %d\n",(long)getpid(),gvar,var);
        return 0;
    }
}

運行結果如下

gcc -g -o vfork2 vfork2.c
./vfork2
process id:10239
gvar = 2 var = 5
the child process id:10240
gvar = 1 var = 6
the parrent process id:10239
gvar = 1 var = 6

3.exec()函數族

通過調用fork()函數和vfork()函數創建子進程,子進程和父進程執行的代碼是相同的。但是,通常創建了一個新進程也就是子進程後,目的時要執行與父進程不同的操作,實現不同的功能。因此,Linux系統提供了一個exec()函數族,用於創建和修改子進程。調用exec()函數時,子進程中的代碼段、數據段和堆棧段都將被替換。由於調用exec()函數並沒有創建新進程,因此修改後的子進程的ID並沒有改變。
exec()函數族由6種以exec開頭的函數組成,定義形式分別如下:

int execl(const char* path,const char* arg,...);
int execlp(const char* file,const char* arg,...);
int execle(const char* path,const char* arg,...,char* const envp[]);
int execv(const char* path,const char* argv[]);
int execve(const char* path,const char* argv[],char* const envp[]);
int execvp(const char* file,const char* argv[]);

這些函數都定義在系統函數庫中,在使用前需要引用頭文件sys/types.h 和 unistd.h,並且必須在預定義時定義一個外部的全局變量,例如

extern char** environ;

上面定義的變量是一個指向Linux系統全局變量的指針。定義了這個變量後,就可以在當前工作目錄中執行系統程序,如同在shell中不輸入路徑直接運行vim和Emacs等程序一樣。
exec()函數族中的函數都實現了對子進程中的數據段、代碼段和堆棧段進行替換的功能,如果調用成功,則加載新的程序,沒有返回值。如果調用出錯,則返回值爲-1。

/*******execve.c文件**********/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
extern char** environ;
int main(int argc,char* argv[])
{
    execve("new",argv,environ);
    puts("this information will not be output in normal situation!");
}
/*************new2.c文件*************/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
    puts("welcome to linux!");
    return 0;
}

運行結果如下:

gcc -o execve execve.c
gcc -o new new2.c
./execve
welcome to linux!

在這個運行結果中,只輸出了“welcome to linux!”,說明在execve.c這個程序中執行了new2.c這個程序中的代碼,那麼execve.c程序中的“this information will not be output in normal situation!”這句話爲什麼沒有正常輸出呢?原因就是調用了execve()函數,調用了這個函數後,將進程中的代碼段、數據段和堆棧段都進行了修改,使得這個新創建的子進程只執行了新加載的這個程序的代碼,此時父進程與子進程的代碼不再有任何關係。執行了execve()函數後,原來存在的代碼都被釋放了,即execve.c這個文件中的puts(“this information will not be output in normal situation!”)代碼得不到執行,因此無法輸出此信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章