当你在 Linux 上启动一个进程时会发生什么？
本文是关于 fork 和 exec 是如何在 Unix 上工作的。你或许已经知道，也有人还不知道。几年前当我了解到这些时，我惊叹不已。
进程 fork exec
本文是关于 fork 和 exec 是如何在 Unix 上工作的。你或许已经知道，也有人还不知道。几年前当我了解到这些时，我惊叹不已。
![](/data/attachment/album/201801/19/113216ou3o0ouab9z01lxx.jpg)


本文是关于 fork 和 exec 是如何在 Unix 上工作的。你或许已经知道，也有人还不知道。几年前当我了解到这些时，我惊叹不已。


我们要做的是启动一个进程。我们已经在博客上讨论了很多关于**系统调用**的问题，每当你启动一个进程或者打开一个文件，这都是一个系统调用。所以你可能会认为有这样的系统调用：



```
start_process(["ls", "-l", "my_cool_directory"])

```

这是一个合理的想法，显然这是它在 DOS 或 Windows 中的工作原理。我想说的是，这并不是 Linux 上的工作原理。但是，我查阅了文档，确实有一个 [posix\_spawn](http://man7.org/linux/man-pages/man3/posix_spawn.3.html) 的系统调用基本上是这样做的，不过这不在本文的讨论范围内。


### fork 和 exec


Linux 上的 `posix_spawn` 是通过两个系统调用实现的，分别是 `fork` 和 `exec`（实际上是 `execve`），这些都是人们常常使用的。尽管在 OS X 上，人们使用 `posix_spawn`，而 `fork` 和 `exec` 是不提倡的，但我们将讨论的是 Linux。


Linux 中的每个进程都存在于“进程树”中。你可以通过运行 `pstree` 命令查看进程树。树的根是 `init`，进程号是 1。每个进程（`init` 除外）都有一个父进程，一个进程都可以有很多子进程。


所以，假设我要启动一个名为 `ls` 的进程来列出一个目录。我是不是只要发起一个进程 `ls` 就好了呢？不是的。


我要做的是，创建一个子进程，这个子进程是我（`me`）本身的一个克隆，然后这个子进程的“脑子”被吃掉了，变成 `ls`。


开始是这样的：



```
my parent
    |- me

```

然后运行 `fork()`，生成一个子进程，是我（`me`）自己的一份克隆：



```
my parent
    |- me
       |-- clone of me

```

然后我让该子进程运行 `exec("ls")`，变成这样：



```
my parent
    |- me
       |-- ls

```

当 ls 命令结束后，我几乎又变回了我自己：



```
my parent
    |- me
       |-- ls (zombie)

```

在这时 `ls` 其实是一个僵尸进程。这意味着它已经死了，但它还在等我，以防我需要检查它的返回值（使用 `wait` 系统调用）。一旦我获得了它的返回值，我将再次恢复独自一人的状态。



```
my parent
    |- me

```

### fork 和 exec 的代码实现


如果你要编写一个 shell，这是你必须做的一个练习（这是一个非常有趣和有启发性的项目。Kamal 在 Github 上有一个很棒的研讨会：<https://github.com/kamalmarhubi/shell-workshop>）。


事实证明，有了 C 或 Python 的技能，你可以在几个小时内编写一个非常简单的 shell，像 bash 一样。（至少如果你旁边能有个人多少懂一点，如果没有的话用时会久一点。）我已经完成啦，真的很棒。


这就是 `fork` 和 `exec` 在程序中的实现。我写了一段 C 的伪代码。请记住，[fork 也可能会失败哦。](https://rachelbythebay.com/w/2014/08/19/fork/)



```
int pid = fork();
// 我要分身啦
// “我”是谁呢？可能是子进程也可能是父进程
if (pid == 0) {
    // 我现在是子进程
    // “ls” 吃掉了我脑子，然后变成一个完全不一样的进程
    exec(["ls"])
} else if (pid == -1) {
    // 天啊，fork 失败了，简直是灾难！
} else {
    // 我是父进程耶
    // 继续做一个酷酷的美男子吧
    // 需要的话，我可以等待子进程结束
}

```

### 上文提到的“脑子被吃掉”是什么意思呢？


进程有很多属性：


* 打开的文件（包括打开的网络连接）
* 环境变量
* 信号处理程序（在程序上运行 Ctrl + C 时会发生什么？）
* 内存（你的“地址空间”）
* 寄存器
* 可执行文件（`/proc/$pid/exe`）
* cgroups 和命名空间（与 Linux 容器相关）
* 当前的工作目录
* 运行程序的用户
* 其他我还没想到的


当你运行 `execve` 并让另一个程序吃掉你的脑子的时候，实际上几乎所有东西都是相同的！ 你们有相同的环境变量、信号处理程序和打开的文件等等。


唯一改变的是，内存、寄存器以及正在运行的程序，这可是件大事。


### 为何 fork 并非那么耗费资源（写入时复制）


你可能会问：“如果我有一个使用了 2GB 内存的进程，这是否意味着每次我启动一个子进程，所有 2 GB 的内存都要被复制一次？这听起来要耗费很多资源！”


事实上，Linux 为 `fork()` 调用实现了<ruby> 写时复制 <rt>  copy on write </rt></ruby>，对于新进程的 2GB 内存来说，就像是“看看旧的进程就好了，是一样的！”。然后，当如果任一进程试图写入内存，此时系统才真正地复制一个内存的副本给该进程。如果两个进程的内存是相同的，就不需要复制了。


### 为什么你需要知道这么多


你可能会说，好吧，这些细节听起来很厉害，但为什么这么重要？关于信号处理程序或环境变量的细节会被继承吗？这对我的日常编程有什么实际影响呢？


有可能哦！比如说，在 Kamal 的博客上有一个很有意思的 [bug](http://kamalmarhubi.com/blog/2015/06/30/my-favourite-bug-so-far-at-the-recurse-center/)。它讨论了 Python 如何使信号处理程序忽略了 `SIGPIPE`。也就是说，如果你从 Python 里运行一个程序，默认情况下它会忽略 `SIGPIPE`！这意味着，程序从 Python 脚本和从 shell 启动的表现会**有所不同**。在这种情况下，它会造成一个奇怪的问题。


所以，你的程序的环境（环境变量、信号处理程序等）可能很重要，都是从父进程继承来的。知道这些，在调试时是很有用的。




---


via: <https://jvns.ca/blog/2016/10/04/exec-will-eat-your-brain/>


作者：[Julia Evans](https://jvns.ca) 译者：[jessie-pang](https://github.com/jessie-pang) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出

