# 分叉,第 1 部分:簡介
> 原文:[Forking, Part 1: Introduction](https://github.com/angrave/SystemProgramming/wiki/Forking%2C-Part-1%3A-Introduction)
> 校驗:[飛龍](https://github.com/wizardforcel)
> 自豪地采用[谷歌翻譯](https://translate.google.cn/)
## 一句警告
進程分叉是一個非常強大(非常危險)的工具。如果你陷入困境并造成一個分叉炸彈(本頁后面會有解釋),**你可能關閉整個系統**。為了減少這種情況,可以通過在命令行中鍵入`ulimit -u 40`將最大進程數限制為較小的數量,例如 40。請注意,此限制僅適用于用戶,這意味著如果您分叉了炸彈,那么您將無法殺死剛剛創建的所有進程,因為調用`killall`需要 shell 來 `fork()`...很諷刺對嗎?那么我們可以做些什么呢。一種解決方案是在另一個用戶(例如 root)之前生成另一個 shell 實例,然后從那里殺死進程。另一種方法是使用內置的`exec`命令來殺死所有用戶進程(小心你只有一次機會)。最后你可以重啟系統:)
在測試`fork()`代碼時,請確保您對所涉及的計算機具有 root 和/或物理訪問權限。如果您必須遠程處理`fork()`代碼,請記住`kill -9 -1`將在緊急情況下為您節省時間。
太長不看:如果您沒有為此做好準備,那么會**非常**危險。**警告過你了**。
## 分叉介紹
## `fork`做什么?
系統調用`fork`克隆當前進程以創建新進程。它通過復制現有進程的狀態創建一個新進程(子進程),但存在一些細微差別(下面討論)。子進程不是從`main`開始的。相反,它就像父進程一樣從`fork()`返回。
## 什么是最簡單的`fork()`示例?
這是一個非常簡單的例子......
```c
printf("I'm printed once!\n");
fork();
// Now there are two processes running
// and each process will print out the next line.
printf("You see this line twice!\n");
```
## 為什么這個例子打印 42?
以下程序打印出 42 - 但`fork()`在`printf`之后!為什么?
```c
#include <unistd.h> /*fork declared here*/
#include <stdio.h> /* printf declared here*/
int main() {
int answer = 84 >> 1;
printf("Answer: %d", answer);
fork();
return 0;
}
```
`printf`行僅執行一次,但是注意到打印內容沒有刷新到標準輸出(沒有打印換行符,我們沒有調用`fflush`或更改緩沖模式)。因此,輸出文本仍在進程內存中等待發送。執行`fork()`時,將復制整個進程內存,包括緩沖區。因此,子進程以非空輸出緩沖區開始,該緩沖區將在程序退出時刷新。
## 如何編寫父子進程不同的代碼?
檢查`fork()`的返回值。返回值`-1` = 失敗; `0` = 在子進程中;正數 = 在父進程中(返回值是子進程 id)。這是一種記住哪種情況的方法:
子進程可以通過調用`getppid()`找到其父進程 - 被復制的原始進程 - 因此不需要來自`fork()`的任何其他返回信息。然而,父進程只能從`fork`的返回值中找出新子進程的 id:
```c
pid_t id = fork();
if (id == -1) exit(1); // fork failed
if (id > 0)
{
// I'm the original parent and
// I just created a child process with id 'id'
// Use waitpid to wait for the child to finish
} else { // returned zero
// I must be the newly made child process
}
```
## 什么是分叉炸彈?
當您嘗試創建無限數量的進程時,就是“分叉炸彈”。一個簡單的例子如下所示:
```c
while (1) fork();
```
這通常會使系統幾乎停滯不前,因為它試圖將 CPU 時間和內存分配給準備運行的大量進程。注釋:系統管理員不喜歡分叉炸彈,并且可能對每個用戶可以擁有的進程數量設置上限,或者可能撤銷登錄權限,因為它會對其他用戶的程序產生干擾。您還可以使用`setrlimit()`限制創建的子進程數。
分叉炸彈不一定是惡意的 - 它們偶爾會因學生編碼錯誤而發生。
Angrave 認為,Matrix 三部曲,機器和人終于共同努力擊敗不斷復制的 Agent-Smith,是基于 AI 驅動的分叉炸彈的電影情節。
## 等待和執行
## 父進程如何等待子進程完成?
使用`waitpid`(或`wait`)。
```c
pid_t child_id = fork();
if (child_id == -1) { perror("fork"); exit(EXIT_FAILURE);}
if (child_id > 0) {
// We have a child! Get their exit code
int status;
waitpid( child_id, &status, 0 );
// code not shown to get exit status from child
} else { // In child ...
// start calculation
exit(123);
}
```
## 我可以讓子進程執行另一個程序嗎?
是。在分叉后使用[`exec`](http://man7.org/linux/man-pages/man3/exec.3.html)函數之一。 `exec`函數集將進程映像替換為所調用的進程映像。這意味著`exec`調用后的任何代碼行都被更換。您希望子進程執行的任何其他工作,都應在`exec`調用之前完成。
[這篇維基百科文章](https://en.wikipedia.org/wiki/Exec_(system_call)#C_language_prototypes)幫助您理解了`exec`家族的名字。
命名方案可以像這樣縮寫:
> 每個的基礎是`exec`(執行),后跟一個或多個字母:
>
> `e` - 指向環境變量的指針數組顯式傳遞給新的進程映像。
>
> `l` - 命令行參數單獨傳遞(列表)到函數。
>
> `p` - 使用`PATH`環境變量查找要執行的文件參數中指定的文件。
>
> `v` - 命令行參數作為指針的數組(向量)傳遞給函數。
```c
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char**argv) {
pid_t child = fork();
if (child == -1) return EXIT_FAILURE;
if (child) { /* I have a child! */
int status;
waitpid(child , &status ,0);
return EXIT_SUCCESS;
} else { /* I am the child */
// Other versions of exec pass in arguments as arrays
// Remember first arg is the program name
// Last arg must be a char pointer to NULL
execl("/bin/ls", "ls","-alh", (char *) NULL);
// If we get to this line, something went wrong!
perror("exec failed!");
}
}
```
## 執行另一個程序的更簡單方法
使用`system`。以下是如何使用它:
```c
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char**argv) {
system("ls");
return 0;
}
```
`system`調用將分叉,執行參數傳遞的命令,原始父進程將等待此操作完成。這也意味著`system`是一個阻塞調用:在`system`創建的進程退出之前,父進程無法繼續。這可能有用也可能沒用。此外,`system`實際上創建了一個 shell,然后給出了字符串,這比直接使用`exec`開銷更大。標準 shell 將使用`PATH`環境變量來搜索與命令匹配的文件名。對于許多簡單的執行某個命令的問題,使用系統通常就足夠了,但很快就會被更復雜或微妙的問題限制,它隱藏了`fork-exec-wait`模式的機制,所以我們鼓勵你學習和使用`fork`,`exec`和`waitpid`來代替。
## 什么是最愚蠢的分叉示例?
一個稍微愚蠢的例子如下所示。它會打印什么?嘗試使用程序的多個參數。
```c
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv) {
pid_t id;
int status;
while (--argc && (id=fork())) {
waitpid(id,&status,0); /* Wait for child*/
}
printf("%d:%s\n", argc, argv[argc]);
return 0;
}
```
驚人的并行表觀 - O(N) 睡眠排序是今天愚蠢的贏家。首次發表于 [4chan 2011](https://dis.4chan.org/read/prog/1295544154) 。這個糟糕但有趣的排序算法的一個版本如下所示。
```c
int main(int c, char **v)
{
while (--c > 1 && !fork());
int val = atoi(v[c]);
sleep(val);
printf("%d\n", val);
return 0;
}
```
注意:由于系統調度程序的工作原理,算法實際上不是`O(N)`。雖然每個進程都有以`O(log(N))`運行的并行算法,但遺憾的是這不是其中之一。
## 子進程與父進程有什么不同?
主要區別包括:
* `getpid()`返回的進程 ID。 `getppid()`返回的父進程 ID。
* 當子進程完成時,通過信號 SIGCHLD 通知父進程,反之則不然。
* 子進程不會繼承待定信號或計時器警報。有關完整列表,請參見[`fork`手冊頁](http://man7.org/linux/man-pages/man2/fork.2.html)。
## 子進程是否共享打開的文件句柄?
是!實際上,兩個進程都使用相同的底層內核文件描述符。例如,如果一個進程將隨機訪問位置倒回到文件的開頭,則兩個進程都會受到影響。
子進程和父進程分別應該`close`(或`fclose`)它們的文件描述符或文件句柄。
## 怎樣才能找到更多?
閱讀手冊頁!
* [`fork`](http://man7.org/linux/man-pages/man2/fork.2.html)
* [`exec`](http://man7.org/linux/man-pages/man3/exec.3.html)
* [`wait`](http://man7.org/linux/man-pages/man2/wait.2.html)
- 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 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話