Linux 建立子進程執行任務

Linux 操做系統牢牢依賴進程建立來知足用戶的需求。例如,只要用戶輸入一條命令,shell 進程就建立一個新進程,新進程運行 shell 的另外一個拷貝並執行用戶輸入的命令。Linux 系統中經過 fork/vfork 系統調用來建立新進程。本文將介紹如何使用 fork/vfork 系統調用來建立新進程並使用 exec 族函數在新進程中執行任務。linux

fork 系統調用

要建立一個進程,最基本的系統調用是 fork:shell

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

調用 fork 時,系統將建立一個與當前進程相同的新進程。一般將原有的進程稱爲父進程,把新建立的進程稱爲子進程。子進程是父進程的一個拷貝,子進程得到同父進程相同的數據,可是同父進程使用不一樣的數據段和堆棧段。子進程從父進程繼承大多數的屬性,可是也修改一些屬性,下表對比了父子進程間的屬性差別:編程

繼承屬性 差別
uid,gid,euid,egid 進程 ID
進程組 ID 父進程 ID
SESSION ID 子進程運行時間記錄
所打開文件及文件的偏移量 父進程對文件的鎖定
控制終端  
設置用戶 ID 和 設置組 ID 標記位  
根目錄與當前目錄  
文件默認建立的權限掩碼  
可訪問的內存區段  
環境變量及其它資源分配  

下面是一個常見的演示 fork 工做原理的 demo(筆者的環境爲 Ubuntu 16.04 desktop):數組

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    pid_t pid;
    char *message;
    int n;
    pid = fork();
    if(pid < 0)
    {
        perror("fork failed");
        exit(1);
    }
    if(pid == 0)
    {
        printf("This is the child process. My PID is: %d. My PPID is: %d.\n", getpid(), getppid());
    }
    else
    {
        printf("This is the parent process. My PID is %d.\n", getpid());
    }
    return 0;
}

把上面的代碼保存到文件 forkdemo.c 文件中,並執行下面的命令編譯:函數

$ gcc forkdemo.c -o forkdemo

而後運行編譯出來的 forkdemo 程序:學習

$ ./forkdemo

fork 函數的特色是 "調用一次,返回兩次":在父進程中調用一次,在父進程和子進程中各返回一次。在父進程中返回時的返回值爲子進程的 PID,而在子進程中返回時的返回值爲 0,而且返回後都將執行 fork 函數調用以後的語句。若是 fork 函數調用失敗,則返回值爲 -1。
咱們細想會發現,fork 函數的返回值設計仍是很高明的。在子進程中 fork 函數返回 0,那麼子進程仍然能夠調用 getpid 函數獲得本身的 PID,也能夠調用 getppid 函數獲得父進程 PID。在父進程中用 getpid 函數能夠獲得本身的 PID,若是想獲得子進程的PID,惟一的辦法就是把 fork 函數的返回值記錄下來。
注意:執行 forkdemo 程序時的輸出是會發生變化的,可能先打印父進程的信息,也可能先打印子進程的信息。ui

vfork 系統調用

vfork 系統調用和 fork 系統調用的功能基本相同。vfork 系統調用建立的進程共享其父進程的內存地址空間,可是並不徹底複製父進程的數據段,而是和父進程共享其數據段。爲了防止父進程重寫子進程須要的數據,父進程會被 vfork 調用阻塞,直到子進程退出或執行一個新的程序。因爲調用 vfork 函數時父進程被掛起,因此若是咱們使用 vfork 函數替換 forkdemo 中的 fork 函數,那麼執行程序時輸出信息的順序就不會變化了。this

使用 vfork 建立的子進程通常會經過 exec 族函數執行新的程序。接下來讓咱們先了解下 exec 族函數。spa

exec 族函數

使用 fork/vfork 建立子進程後執行的是和父進程相同的程序(但有可能執行不一樣的代碼分支),子進程每每須要調用一個 exec 族函數以執行另一個程序。當進程調用 exec 族函數時,該進程的用戶空間代碼和數據徹底被新程序替換,重新程序的起始處開始執行。調用 exec 族函數並不建立新進程,因此調用 exec 族函數先後該進程的 PID 並不改變。操作系統

exec 族函數一共有六個:

#include <unistd.h>
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, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

函數名字中帶字母 "l" 的表示其參數個數不肯定,帶字母 "v" 的表示使用字符串數組指針 argv 指向參數列表。
函數名字中含有字母 "p" 的表示能夠自動在環境變量 PATH 指定的路徑中搜索要執行的程序。
函數名字中含有字母 "e" 的函數比其它函數多一個參數 envp。該參數是字符串數組指針,用於指定環境變量。調用這樣的函數時,能夠由用戶自行設定子進程的環境變量,存放在參數 envp 所指向的字符串數組中。

事實上,只有 execve 是真正的系統調用,其它五個函數最終都調用 execve。這些函數之間的關係以下圖所示(此圖來自互聯網):

exec 族函數的特徵:調用 exec 族函數會把新的程序裝載到當前進程中。在調用過 exec 族函數後,進程中執行的代碼就與以前徹底不一樣了,因此 exec 函數調用以後的代碼是不會被執行的。

在子進程中執行任務

下面讓咱們經過 vfork 和 execve 函數實如今子進程中執行 ls 命令:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    pid_t pid;
    if((pid=vfork()) < 0)
    {
        printf("vfork error!\n");
        exit(1);
    }
    else if(pid==0)
    {
        printf("Child process PID: %d.\n", getpid());
        char *argv[ ]={"ls", "-al", "/home", NULL};  
        char *envp[ ]={"PATH=/bin", NULL};
        if(execve("/bin/ls", argv, envp) < 0)
        {
            printf("subprocess error");
            exit(1);
        }
        // 子進程要麼從 ls 命令中退出,要麼從上面的 exit(1) 語句退出
        // 因此代碼的執行路徑永遠也走不到這裏,下面的 printf 語句不會被執行
        printf("You should never see this message.");
    }
    else
    {
        printf("Parent process PID: %d.\n", getpid());
        sleep(1);
    }
    return 0;
}

把上面的代碼保存到文件 subprocessdemo.c 文件中,並執行下面的命令編譯:

$ gcc subprocessdemo.c -o subprocessdemo

而後運行編譯出來的 subprocessdemo程序:

$ ./subprocessdemo

總結

fork/vfork 函數和 exec 族函數都是 Linux 系統中很是重要的概念。本文試圖經過簡單的 demo 來演示這些函數的基本用法,爲理解 Linux 系統中父進程與子進程的概念提供一些直觀的感覺。

 

參考:

Linux C 編程一站式學習《Linux 環境下 C 編程指南》《深刻理解 Linux 內核》

相關文章
相關標籤/搜索