# 分叉，第 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)