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 和 設定組 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 系統中父程式與子程式的概念提供一些直觀的感受。
參考:
Linux C 程式設計一站式學習
《Linux 環境下 C 程式設計指南》
《深入理解 Linux 核心》