# 分叉,第 2 部分:Fork,Exec,等等
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Forking%2C-Part-2%3A-Fork%2C-Exec%2C-Wait>
## 模式
## 以下'exec'示例有什么作用?
```c
#include <unistd.h>
#include <fcntl.h> // O_CREAT, O_APPEND etc. defined here
int main() {
close(1); // close standard out
open("log.txt", O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
puts("Captain's log");
chdir("/usr/include");
// execl( executable, arguments for executable including program name and NULL at the end)
execl("/bin/ls", /* Remaining items sent to ls*/ "/bin/ls", ".", (char *) NULL); // "ls ."
perror("exec failed");
return 0; // Not expected
}
```
上面的代碼中沒有錯誤檢查(我們假設 close,open,chdir 等按預期工作)。
* open:將使用最低可用文件描述符(即 1);如此標準現在轉到日志文件。
* chdir:將當前目錄更改為/ usr / include
* execl:用/ bin / ls 替換程序映像并調用其 main()方法
* perror:我們不希望到達這里 - 如果我們這樣做,那么 exec 就失敗了。
## 微妙的分叉蟲
這段代碼出了什么問題
```c
#include <unistd.h>
#define HELLO_NUMBER 10
int main(){
pid_t children[HELLO_NUMBER];
int i;
for(i = 0; i < HELLO_NUMBER; i++){
pid_t child = fork();
if(child == -1){
break;
}
if(child == 0){ //I am the child
execlp("ehco", "echo", "hello", NULL);
}
else{
children[i] = child;
}
}
int j;
for(j = 0; j < i; j++){
waitpid(children[j], NULL, 0);
}
return 0;
}
```
我們錯誤拼寫了`ehco`,所以我們不能`exec`它。這是什么意思?我們只是創建了 2 ** 10 個進程,而不是創建 10 個進程,而是對我們的機器進行轟炸。我們怎么能阻止這個?在 exec 之后立即退出,以防 exec 失敗,我們不會最終轟炸我們的機器。
## 孩子從父母那里繼承了什么?
* 打開文件句柄。如果父母后來尋求回到文件的開頭那么這也會影響孩子(反之亦然)。
* 信號處理程序
* 當前的工作目錄
* 環境變量
有關詳細信息,請參見 [fork 手冊頁](http://linux.die.net/man/2/fork)。
## 子進程與父進程有什么不同?
進程 ID 不同。在調用`getppid()`的子代中(注意兩個'p')將給出與在父代中調用 getpid()相同的結果。有關更多詳細信息,請參見 fork 手冊頁。
## 我該如何等待孩子完成?
使用`waitpid`或`wait`。父進程將暫停,直到`wait`(或`waitpid`)返回。請注意,這個解釋掩蓋了重新開始的討論。
## 什么是 fork-exec-wait 模式
常見的編程模式是調用`fork`,然后調用`exec`和`wait`。原始進程調用 fork,它創建一個子進程。然后,子進程使用 exec 開始執行新程序。同時父母使用`wait`(或`waitpid`)等待子進程完成。請參閱下面的完整代碼示例。
## 如何啟動同時運行的后臺進程?
不要等他們!您的父進程可以繼續執行代碼,而無需等待子進程。注意在實踐中,通過在調用 exec 之前調用打開的文件描述符上的`close`,后臺進程也可以與父進程和輸出流斷開連接。
但是,在父完成之前完成的子進程可能會變成僵尸。有關更多信息,請參閱僵尸頁面。
## 植物大戰僵尸
## 好父母不要讓自己的孩子成為僵尸!
當一個孩子完成(或終止)時,它仍占用內核進程表中的一個槽。只有當孩子'等待'時,才能再次使用該插槽。
一個長期運行的程序可以通過不斷創建進程來創建許多僵尸,而不會為它們進行`wait`處理。
## 太多僵尸會有什么影響?
最終,內核進程表中沒有足夠的空間來創建新進程。因此`fork()`會失敗并且可能使系統難以/不可能使用 - 例如只需登錄就需要新的進程!
## 系統如何幫助預防僵尸?
一旦一個進程完成,它的任何子進程都將被分配給“init” - 第一個進程的 pid 為 1.因此這些孩子會看到 getppid()返回值為 1.這些孤兒最終會完成,并在短時間內成為一個僵尸。幸運的是,init 進程自動等待其所有子進程,從而從系統中刪除這些僵尸。
## 我該如何預防僵尸? (警告:簡化回答)
等你的孩子!
```c
waitpid(child, &status, 0); // Clean up and wait for my child process to finish.
```
請注意,我們假設獲得 SIGCHLD 事件的唯一原因是孩子已經完成(這不完全正確 - 請參閱手冊頁以獲取更多詳細信息)。
強大的實現還可以檢查中斷狀態并將上述內容包含在循環中。繼續閱讀,討論更強大的實現。
## 我怎樣才能異步等待使用 SIGCHLD 的孩子? (高級)
警告:本節使用的信號尚未完全介紹。當子進程完成時,父進程獲取信號 SIGCHLD,因此信號處理程序可以等待進程。稍微簡化的版本如下所示。
```c
pid_t child;
void cleanup(int signal) {
int status;
waitpid(child, &status, 0);
write(1,"cleanup!\n",9);
}
int main() {
// Register signal handler BEFORE the child can finish
signal(SIGCHLD, cleanup); // or better - sigaction
child = fork();
if (child == -1) { exit(EXIT_FAILURE);}
if (child == 0) { /* I am the child!*/
// Do background stuff e.g. call exec
} else { /* I'm the parent! */
sleep(4); // so we can see the cleanup
puts("Parent is done");
}
return 0;
}
```
然而,上面的例子忽略了幾個微妙的要點:
* 不止一個孩子可能已經完成但父母只會獲得一個 SIGCHLD 信號(信號沒有排隊)
* 可以出于其他原因發送 SIGCHLD 信號(例如暫時停止子進程)
收獲僵尸的更強大的代碼如下所示。
```c
void cleanup(int signal) {
int status;
while (waitpid((pid_t) (-1), 0, WNOHANG) > 0) {}
}
```
## 那么什么是環境變量?
環境變量是系統為所有進程保留的變量。您的系統現在已經設置好了!在 Bash 中,您可以查看其中的一些內容
```
$ echo $HOME
/home/bhuvy
$ echo $PATH
/usr/local/sbin:/usr/bin:...
```
你會如何在 C / C ++中獲得這些?您可以使用`getenv`和`setenv`功能
```c
char* home = getenv("HOME"); // Will return /home/bhuvy
setenv("HOME", "/home/bhuvan", 1 /*set overwrite to true*/ );
```
## 是的,那么這些環境變量如何對父母/孩子意味著什么呢?
那么每個進程都會獲得自己的環境變量字典,并將其復制到子進程中。這意味著,如果父級更改其環境變量,則不會將其傳輸給子級,反之亦然。如果你想用不同于父(或任何其他進程)的環境變量執行程序,這在 fork-exec-wait 三部曲中很重要。
例如,您可以編寫一個循環遍歷所有時區的 C 程序,并執行`date`命令以打印所有本地的日期和時間。環境變量用于各種程序,因此修改它們很重要。
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話