linux 操作系統(tǒng)緊緊依賴進(jìn)程創(chuàng)建來滿足用戶的需求。例如,只要用戶輸入一條命令,shell 進(jìn)程就創(chuàng)建一個新進(jìn)程,新進(jìn)程運行 shell 的另一個拷貝并執(zhí)行用戶輸入的命令。linux 系統(tǒng)中通過 fork/vfork 系統(tǒng)調(diào)用來創(chuàng)建新進(jìn)程。本文將介紹如何使用 fork/vfork 系統(tǒng)調(diào)用來創(chuàng)建新進(jìn)程并使用 exec 族函數(shù)在新進(jìn)程中執(zhí)行任務(wù)。
fork 系統(tǒng)調(diào)用
要創(chuàng)建一個進(jìn)程,最基本的系統(tǒng)調(diào)用是 fork:
1
2
3
|
# include < unistd.h > pid_t fork(void); pid_t vfork(void); |
調(diào)用 fork 時,系統(tǒng)將創(chuàng)建一個與當(dāng)前進(jìn)程相同的新進(jìn)程。通常將原有的進(jìn)程稱為父進(jìn)程,把新創(chuàng)建的進(jìn)程稱為子進(jìn)程。子進(jìn)程是父進(jìn)程的一個拷貝,子進(jìn)程獲得同父進(jìn)程相同的數(shù)據(jù),但是同父進(jìn)程使用不同的數(shù)據(jù)段和堆棧段。子進(jìn)程從父進(jìn)程繼承大多數(shù)的屬性,但是也修改一些屬性,下表對比了父子進(jìn)程間的屬性差異:
繼承屬性 | 差異 |
uid,gid,euid,egid | 進(jìn)程 id |
進(jìn)程組 id | 父進(jìn)程 id |
session id | 子進(jìn)程運行時間記錄 |
所打開文件及文件的偏移量 | 父進(jìn)程對文件的鎖定 |
控制終端 | |
設(shè)置用戶 id 和 設(shè)置組 id 標(biāo)記位 | |
根目錄與當(dāng)前目錄 | |
文件默認(rèn)創(chuàng)建的權(quán)限掩碼 | |
可訪問的內(nèi)存區(qū)段 | |
環(huán)境變量及其它資源分配 |
下面是一個常見的演示 fork 工作原理的 demo(筆者的環(huán)境為 ubuntu 16.04 desktop):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#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 文件中,并執(zhí)行下面的命令編譯:
1
|
$ gcc forkdemo.c -o forkdemo |
然后運行編譯出來的 forkdemo 程序:
1
|
$ ./forkdemo |
fork 函數(shù)的特點是 "調(diào)用一次,返回兩次":在父進(jìn)程中調(diào)用一次,在父進(jìn)程和子進(jìn)程中各返回一次。在父進(jìn)程中返回時的返回值為子進(jìn)程的 pid,而在子進(jìn)程中返回時的返回值為 0,并且返回后都將執(zhí)行 fork 函數(shù)調(diào)用之后的語句。如果 fork 函數(shù)調(diào)用失敗,則返回值為 -1。
我們細(xì)想會發(fā)現(xiàn),fork 函數(shù)的返回值設(shè)計還是很高明的。在子進(jìn)程中 fork 函數(shù)返回 0,那么子進(jìn)程仍然可以調(diào)用 getpid 函數(shù)得到自己的 pid,也可以調(diào)用 getppid 函數(shù)得到父進(jìn)程 pid。在父進(jìn)程中用 getpid 函數(shù)可以得到自己的 pid,如果想得到子進(jìn)程的pid,唯一的辦法就是把 fork 函數(shù)的返回值記錄下來。
注意:執(zhí)行 forkdemo 程序時的輸出是會發(fā)生變化的,可能先打印父進(jìn)程的信息,也可能先打印子進(jìn)程的信息。
vfork 系統(tǒng)調(diào)用
vfork 系統(tǒng)調(diào)用和 fork 系統(tǒng)調(diào)用的功能基本相同。vfork 系統(tǒng)調(diào)用創(chuàng)建的進(jìn)程共享其父進(jìn)程的內(nèi)存地址空間,但是并不完全復(fù)制父進(jìn)程的數(shù)據(jù)段,而是和父進(jìn)程共享其數(shù)據(jù)段。為了防止父進(jìn)程重寫子進(jìn)程需要的數(shù)據(jù),父進(jìn)程會被 vfork 調(diào)用阻塞,直到子進(jìn)程退出或執(zhí)行一個新的程序。由于調(diào)用 vfork 函數(shù)時父進(jìn)程被掛起,所以如果我們使用 vfork 函數(shù)替換 forkdemo 中的 fork 函數(shù),那么執(zhí)行程序時輸出信息的順序就不會變化了。
使用 vfork 創(chuàng)建的子進(jìn)程一般會通過 exec 族函數(shù)執(zhí)行新的程序。接下來讓我們先了解下 exec 族函數(shù)。
exec 族函數(shù)
使用 fork/vfork 創(chuàng)建子進(jìn)程后執(zhí)行的是和父進(jìn)程相同的程序(但有可能執(zhí)行不同的代碼分支),子進(jìn)程往往需要調(diào)用一個 exec 族函數(shù)以執(zhí)行另外一個程序。當(dāng)進(jìn)程調(diào)用 exec 族函數(shù)時,該進(jìn)程的用戶空間代碼和數(shù)據(jù)完全被新程序替換,從新程序的起始處開始執(zhí)行。調(diào)用 exec 族函數(shù)并不創(chuàng)建新進(jìn)程,所以調(diào)用 exec 族函數(shù)前后該進(jìn)程的 pid 并不改變。
exec 族函數(shù)一共有六個:
1
2
3
4
5
6
7
|
#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[]); |
函數(shù)名字中帶字母 "l" 的表示其參數(shù)個數(shù)不確定,帶字母 "v" 的表示使用字符串?dāng)?shù)組指針 argv 指向參數(shù)列表。
函數(shù)名字中含有字母 "p" 的表示可以自動在環(huán)境變量 path 指定的路徑中搜索要執(zhí)行的程序。
函數(shù)名字中含有字母 "e" 的函數(shù)比其它函數(shù)多一個參數(shù) envp。該參數(shù)是字符串?dāng)?shù)組指針,用于指定環(huán)境變量。調(diào)用這樣的函數(shù)時,可以由用戶自行設(shè)定子進(jìn)程的環(huán)境變量,存放在參數(shù) envp 所指向的字符串?dāng)?shù)組中。
事實上,只有 execve 是真正的系統(tǒng)調(diào)用,其它五個函數(shù)最終都調(diào)用 execve。這些函數(shù)之間的關(guān)系如下圖所示(此圖來自互聯(lián)網(wǎng)):
exec 族函數(shù)的特征:調(diào)用 exec 族函數(shù)會把新的程序裝載到當(dāng)前進(jìn)程中。在調(diào)用過 exec 族函數(shù)后,進(jìn)程中執(zhí)行的代碼就與之前完全不同了,所以 exec 函數(shù)調(diào)用之后的代碼是不會被執(zhí)行的。
在子進(jìn)程中執(zhí)行任務(wù)
下面讓我們通過 vfork 和 execve 函數(shù)實現(xiàn)在子進(jìn)程中執(zhí)行 ls 命令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#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); } // 子進(jìn)程要么從 ls 命令中退出,要么從上面的 exit(1) 語句退出 // 所以代碼的執(zhí)行路徑永遠(yuǎn)也走不到這里,下面的 printf 語句不會被執(zhí)行 printf("you should never see this message."); } else { printf("parent process pid: %d.\n", getpid()); sleep(1); } return 0; } |
把上面的代碼保存到文件 subprocessdemo.c 文件中,并執(zhí)行下面的命令編譯:
1
|
$ gcc subprocessdemo.c -o subprocessdemo |
然后運行編譯出來的 subprocessdemo程序:
1
|
$ ./subprocessdemo |
總結(jié)
fork/vfork 函數(shù)和 exec 族函數(shù)都是 linux 系統(tǒng)中非常重要的概念。本文試圖通過簡單的 demo 來演示這些函數(shù)的基本用法,為理解 linux 系統(tǒng)中父進(jìn)程與子進(jìn)程的概念提供一些直觀的感受。
原文鏈接:https://www.cnblogs.com/sparkdev/p/8214455.html