> For the complete documentation index, see [llms.txt](https://book.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-er-bu-fen-jin-cheng/5.7.-xin-jin-cheng-de-chuang-jian.md).

# 5.7 新进程的创建

UNIX 进程用 `fork` 系统调用创建。`fork` 之后通常紧跟着 `exec` 系统调用，用文件系统中的可执行映像内容覆盖子进程的虚拟地址空间。进程随后一直执行到终止——或自愿退出，或因收到信号被迫终止。随后几节追踪这一循环各步骤中内存资源的管理。

`fork` 系统调用复制现有进程的地址空间，创建一个一模一样的子进程。`fork` 是 UNIX 中创建新进程的唯一途径（其变体 `vfork` 除外，稍后描述）。4.3BSD 的 `fork` 实现复制原进程的全部资源并复制该进程的地址空间。

必须为子进程分配的虚拟内存资源包括 proc 结构、为进程作后备的辅存、内核页表和用户页表、user 结构以及内核栈。新进程的许多页（父进程中不再是按需填充的那些）会分配到主存，尽管到进程首次运行时可能又被换出。`fork` 实现的总体轮廓如下：

* 为子进程分配辅存
* 为子进程分配一个 proc 表项并填写
* 为子进程分配页表
* 分配新的 user 结构，复制当前 user 结构来初始化
* 复制地址空间：复制按需填充 PTE，复制其余所有页的内容
* 安排向子进程的返回，以区别于父进程

proc 结构的分配与初始化以及返回值的安排在第 4 章讲过。本节其余部分讨论复制进程涉及的其他步骤。

**复制内核资源**

复制地址空间时最先分配的资源是交换区。交换空间最先分配，因为它是最可能告罄的资源。现有进程已为自己的交换空间持有交换映射，新分配必须先记入 user 结构的影子交换映射（shadow swap map），稍后才能装进子进程的 user 结构。这些交换映射 `u_cdmap` 和 `u_csmap` 在 `exec` 期间也要用到；对它们的处理稍后描述。内核分配交换空间的办法是把影子映射清零，再把映射扩展到进程数据和栈段的全部大小。于是进程持有足以装下自己两个完整副本的交换空间，其中一个副本没有内容。

`fork` 的下一步是分配并初始化新的 proc 结构。这一步必须在复制当前进程的地址空间之前完成，因为它要在 proc 结构中记录状态。要在新进程中复制地址空间，内核必须分配页表和主存页。页表的分配由 `vgetpt()` 完成：先从 `kernelmap` 分配系统 PTE 用于映射用户页表，再用 `vmemall()` 从空闲内存池分配装用户页表的内存页。系统页表没有空间时，分配请求会使进程阻塞。系统页表缺乏空间通常是碎片造成的。出现这种情况时，系统只得求助于进程交换来回收资源；这一机制见 5.13 节。

从系统页表分配空间到用户页表分配完毕之间，父进程锁定不得换出，以免死锁。子进程处于不一致状态，既不能运行也不能换出，而复制页表需要父进程。为确保调度器忽略子进程，内核在整个 fork 过程中把该进程的状态设为 SIDL。

虚拟内存资源分配完毕，系统建立新进程的内核态和用户态状态，包括硬件存储管理寄存器和 user 结构。创建 user 结构的副本需要先取得一份一致的 VAX 进程控制块。父进程靠向自己模拟一次上下文切换做到：

```c
    resume(pcbb(u.u_procp));
```

随后，为了能同时访问当前和新的 user 结构，新的 user 结构经名为 `Forkmap` 的专用页映射表映射进内核虚拟内存的一个专用窗口。父子的 user 结构都能在内核虚拟地址空间寻址后，即可把当前 user 结构复制到新的，并更新新 user 结构的虚拟内存部分以反映子进程的页表和交换空间分配。

**复制用户地址空间**

子进程的内核状态建好后，就可以复制用户地址空间的其余部分了。正文部分简单：正文结构的引用计数加一，表示又有一个进程共享它；页表从当前使用该正文段的进程复制。

最后，数据和栈区逐页复制。仍是按需填充的页只须复制 PTE。其余所有页，内核为新进程分配内存，把数据从当前进程复制到新的内存页。复制从用户虚拟地址做到物理地址，因为父进程访问不到新的地址空间。机器语言例程 `copyseg()` 做这个复制：用一个保留的系统映射表项映射目标页，再复制数据。源页若不驻留内存，复制会引起缺页；缺页正常服务，返回后复制继续。（注意，这是内核中少数几处内核态故障不按系统失败处理的地方之一。）新 PTE 置为有效并标记为已修改，因为对应的交换块还没有正确的内容。

**实现问题**

大进程 fork 时，复制整个用户地址空间代价高昂。所有在辅存上的页都必须读回内存才能复制。如果两个完整副本的空闲内存不够，这一内存短缺会使系统开始调页，凑出做复制的内存（见 5.12 节）。结果可能把父子进程的部分页调出，也殃及无关进程的部分页。人们考虑过其他创建进程的办法来避免这种开销。

一种曾考虑纳入 4.2BSD 而未实现的技术叫写时复制（copy-on-write）。fork 的页不再逐页复制，fork 产生的每个进程都拿到对相同物理页的引用。页表改为禁止任一进程修改共享页；进程试图修改一页时，内核经一次保护故障取得控制。内核发现故障源自修改共享页的企图后，径直复制该页并把页的保护字段改回允许修改。只有被其中一方修改过的页才需要复制。由于 fork 的进程通常紧接着就用 `exec` 以新映像覆盖子进程，这一技术能显著改善性能。

写时复制未能进入虚拟内存系统的最初设计，有种种务实的理由：它要求逐页维护引用计数——对驻留辅存的页尤其要紧；更重要的是，当时 VAX 某一型号的微码问题使人拿不准写时复制能否可靠实现。于是取而代之纳入了下一小节描述的 `vfork` 系统调用。

**免复制的进程创建**

避免 `fork` 所隐含内存复制的另一种方案干脆取消了复制。进程（比如 shell）想启动另一个程序时，一般会 `fork`，做几个简单操作（重定向 I/O 描述符、改信号动作），然后用 `exec` 启动新程序；与此同时，父 shell 用 `wait` 把自己挂起，直到新程序完成。对这类操作，父子不必同时运行，地址空间一份就够。这种频繁出现的情形催生了 `vfork` 系统调用。`vfork` 效率极高，但语义古怪，一般认为是个体系结构上的瑕疵。

`vfork` 的实现大部分与 `fork` 共享。主要区别是：父进程不再为子复制地址空间，而是把自己的地址空间 **移交** 给子进程并把自己挂起。子进程只需分配新的 proc 和 user 结构，其余全部从父进程接收。子进程从 `vfork` 系统调用返回时父进程仍挂起。子进程照常做启动新程序的准备工作，然后调用 `exec`。此时地址空间交还父进程，而不是像正常 `exec` 那样被抛弃。或者，子进程遇到错误无法执行新程序时将 `exit`，地址空间同样交还父进程而不是被抛弃。这一串事件见图 5.16。

`vfork` 的子进程起初只用一个 user 结构和恰好映射该结构的页表创建。然后父进程与子进程交换地址空间。工作多半在于交换描述地址空间的 proc 和 user 结构中的指针、映射和计数器。页表的交换比较麻烦，因为正在使用的内核栈就在被移交的 user 结构里。移交在父进程的上下文中完成：用 `Forkmap` 映射子进程的 user 结构，把父进程关于 user 结构的 PTE 复制进子进程的页表。等真正靠改硬件页表基址寄存器换掉页表时，父进程的 user 结构仍保有正确的映射。子进程 user 结构的页映射再从 `Forkmap` 里的副本恢复。

把资源移交给子进程之后，父进程给自己的进程表项标上 SNOVM 标志（没有虚拟内存资源），使页面回收时忽略它，同时给子进程的表项标上 SVFORK。然后父进程阻塞，直到子进程用完父进程的资源。子进程调用 `exec` 或 `exit` 时，清除 SVFORK 标志，唤醒父进程，然后入睡等父进程收回地址空间。反向移交与第一次完全同样地做，细心保住 user 结构的正确映射。父进程完成移交后，给子进程标上 SVFDONE 标志并唤醒它。子进程见移交完成，继续其系统调用；无论哪种情况，它要释放的资源都比正常进程少。

```sh
             父进程                        子进程

                                  +---------+
                                  |  页表   |
                                  +---------+
                                  | u. 区   |  新
             +---------+          +---------+
             |  页表   |          |         |
             +---------+          +---------+
             | u. 区   |          |         |
             +---------+          +---------+
状态：开始 *vfork*  |   :     |          |         |
                      |   :     |          +---------+
             +---------+          |         |
             | data    |          +---------+
             +---------+          |         |
             | text    |          +---------+
             +---------+
                                  状态：分配了最少的资源

             +---------+          +---------+
             |  页表   |   <=>    |  页表   |
             +---------+          +---------+
             | u. 区   |          | u. 区   |
             +---------+          +---------+
             |         |   =>     | stack   |
             +---------+          +---------+
状态：SNOVM |   :     |          |   :     |  状态：SVFORK
             |   :     |          |   :     |
             +---------+          +---------+
             |         |   =>     | data    |
             +---------+          +---------+
             |         |   =>     | text    |
             +---------+          +---------+

             +---------+          +---------+
收回的       |  页表   |          |  页表   |  新
             +---------+          +---------+
             | u. 区   |          | u. 区   |  新
             +---------+          +---------+
             | stack   |          | stack   |  新
             +---------+          +---------+
状态：旧映像 |   :     |          |   :     |  状态：*exec* 新
      归还   |   :     |          |   :     |      程序
             +---------+          +---------+
收回的       | data    |          | data    |  新
             +---------+          +---------+
收回的       | text    |          | text    |  新
             +---------+          +---------+
```

图 5.16 `vfork` 的运作

这段对 `vfork` 的描述表明，地址空间的页和页映射本身都无须复制。`vfork` 大概会一直比写时复制和其他必须复制进程虚拟地址空间的方案更高效。`vfork` 调用在体系结构上的怪癖在于：子进程持有控制权期间，可以修改父进程地址空间的内容、甚至大小。虽然修改父进程的地址空间属于糟糕的编程习惯，已知有些程序就钻这个怪癖的空子。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-er-bu-fen-jin-cheng/5.7.-xin-jin-cheng-de-chuang-jian.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
