# 分叉,第 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 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话