Linux 建立子進程執行任務
Linux 作業系統緊緊依賴進程建立來滿足使用者的需求。例如,只要使用者輸入一條命令,shell 進程就建立一個新進程,新進程運行 shell 的另一個拷貝並執行使用者輸入的命令。Linux 系統中通過 fork/vfork 系統調用來建立新進程。本文將介紹如何使用 fork/vfork 系統調用來建立新進程並使用 exec 族函數在新進程中執行任務。
fork 系統調用
要建立一個進程,最基本的系統調用是 fork:
# include <unistd.h>pid_t fork(void);pid_t vfork(void);
調用 fork 時,系統將建立一個與當前進程相同的新進程。通常將原有的進程稱為父進程,把新建立的進程稱為子進程。子進程是父進程的一個拷貝,子進程獲得同父進程相同的資料,但是同父進程使用不同的資料區段和堆棧段。子進程從父進程繼承大多數的屬性,但是也修改一些屬性,下表對比了父子進程間的屬性差異:
| 繼承屬性 |
差異 |
| uid,gid,euid,egid |
進程 ID |
| 進程組 ID |
父進程 ID |
| SESSION 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 程式時的輸出是會發生變化的,可能先列印父進程的資訊,也可能先列印子進程的資訊。
vfork 系統調用
vfork 系統調用和 fork 系統調用的功能基本相同。vfork 系統調用建立的進程共用其父進程的記憶體位址空間,但是並不完全複製父進程的資料區段,而是和父進程共用其資料區段。為了防止父進程重寫子進程需要的資料,父進程會被 vfork 調用阻塞,直到子進程退出或執行一個新的程式。由於調用 vfork 函數時父進程被掛起,所以如果我們使用 vfork 函數替換 forkdemo 中的 fork 函數,那麼執行程式時輸出資訊的順序就不會變化了。
使用 vfork 建立的子進程一般會通過 exec 族函數執行新的程式。接下來讓我們先瞭解下 exec 族函數。
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 系統中父進程與子進程的概念提供一些直觀的感受。