> 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.11.-qing-qiu-diao-ye.md).

# 5.11 请求调页

存储管理硬件转换虚拟地址时发现无效 PTE，就向系统产生一个陷阱。这种缺页陷阱可能出于多种原因。多数 4BSD 程序采用的格式允许可执行映像直接从文件系统调页进入主存。请求调页格式的程序首次运行时，内核把执行进程正文区和已初始化数据区的 PTE 标为无效并按需从文件系统填充。正文区每页首次被引用时都发生一次缺页。进程首次引用程序未初始化数据区的页时也会缺页：系统自动为进程分配内存，并把新分配的页清零。其他类型的缺页来自引用位的仿真，以及先前驻留的页在内存短缺时被系统回收。

缺页的处理围绕 `pagein()` 例程展开，所有缺页都由它服务。`pagein()` 每次被调用都拿到引发故障的虚拟地址。它先找到故障地址的 PTE，把页归类为正文、数据或栈，再确定页的状态。可能的状态有：

* 按需填充
* 驻留但无效
* 已换出

确定了页的类型和状态后，`pagein()` 必须取来所需数据、使故障页对应的 PTE 有效，然后返回以重启进程。以下几小节描述 `pagein()` 对每种可取页类型的动作。

**按需填充页**

对按需填充页的故障，调页系统必须先确定页应含数据的来源。若页是从文件填充，内存空闲表上可能就有该页的副本，可以省去从磁盘读取；见 5.4 节。页在空闲表上找到时，`pagein()` 把它从空闲表摘下用于当前进程，即回收（reclaim）。否则，`pagein()` 必须分配一页主存，填入正确内容，再把物理页的身份记入相应 PTE。零填充页简单清零即可；从正文填充的页必须从辅存取得内容。每个从正文填充的页在辅存上的位置，要么由 PTE 中现成的磁盘块号和设备标识符给出（从文件系统调入时），要么从进程交换映射推导（被换出的页）。这些项加一个页计数就完全标识了要调入的磁盘数据来源。进程身份和页起始处的虚拟地址描述调页操作的目的地。

按需填充页的缺页服务要收尾，需把页标记为有效且已修改。标记为已修改，表示分配给它的交换页还没有正确内容；该页若被选中替换，会先写入交换空间再重用。

**按需填充成组**

为一次从正文填充的缺页服务可能相当费事。幸好多数程序都表现出某种程度的引用局部性：短时间内内存引用集中在虚拟地址空间的局部。进程表现出引用局部性时，预取与故障页相邻的页可以降低进程的缺页率。

每当为从文件填充的缺页服务时，调页系统都试图把文件中与故障页相邻、可能也会用到的页一并读入。这一操作称为按需填充成组（fill-on-demand klustering），由 `fodkluster()` 实现；名字这样拼是为了与页成簇（clustering，见 5.2 节）区分。预取页的读取搭在本来就要做的磁盘传输上顺路完成。预取的小小额外成本，比日后缺页服务时间的改善划算得多。预取的页放在空闲表尾部，进程很快用到即可回收。

```sh
            缺页页
               ↓
| V 0 | V 0 | V 0 | V 0 | V 0 | V 1 |
| F 1 | F 1 | F 1 | F 1 | F 1 | --- |
|BN 90|BN 46|BN 47|BN 48|BN 49| --- |
|←——— 成组的页 ———→|
```

图 5.18 按需填充成组的运作（V 为有效位；F 为按需填充位；BN 为磁盘块号）

成组计算构成一组页簇，含目标页以及虚拟内存和磁盘上都相邻、仍按需填充的簇（见图 5.18）。一组中至多 16 个簇（VAX 上 32 页）。从文件系统读按需填充页时，成组通常读入整个文件系统块（一般 4 或 8 KB），而不是单个页簇（1 KB）。

**与文件系统缓存的互动**

程序的可执行映像总是从文件系统直接调页进入进程的页，绕过文件系统缓存。文件系统使用写回缓存（见第 7 章），可执行文件的磁盘块在文件创建后不久可能还不含正确数据——数据可能仍在文件系统缓冲区高速缓存里。这个问题有两种解法：一是从磁盘调页前先查文件系统缓存，数据在就从缓存复制；二是先从文件系统缓存清洗写滞块，再从磁盘读入。内核采用后者：调页之前强制把写滞块落盘。这一选择是历史原因：系统当年开发的 VAX 计算机受限于 CPU 而 I/O 有富余。让磁盘控制器用 DMA（经磁盘）把数据从缓冲区高速缓存搬到进程正文页，比占用疲惫的 CPU 做内存到内存的复制，能获得更好的系统吞吐。今天权衡已经反转，I/O 通常是瓶颈。极端例子是无盘工作站：经磁盘复制意味着两次跨网络传输。

**换出页的页面换入**

页驻留内存之后可能被换出、失效。进程再次引用该页又将引发缺页。此时要做的事与首次缺页不同：第一步仍是定位故障页的 PTE，但若页不是按需填充且 PTE 中的页号非零，就必须查询相应的核心映射表项来确定页的状态。

不是按需填充且无效的页有四种可能状态：

1. 页是为了采样引用而被置无效的。PTE 中的页号非零（有效），页的核心映射表项未锁定且不在空闲表上：`pagein()` 径直把页标为有效并把控制交还进程。这一操作称为回收（reclaim）。
2. 页已被释放但尚未重用。PTE 中的页号非零，核心映射表项未锁定但在空闲表上：必须先把页从空闲表摘下才能置为有效。这一操作称为从空闲表回收（reclaim from free）。
3. 正文页可能不驻留，但另一个进程可能已对同一页缺页并启动了把它调入内存的读操作。页号非零，核心映射表项标记为锁定且转移中。对这类页，`pagein()` 必须等传输结束：相应的核心映射表结构标记有进程在等这页，然后 `pagein()` 在该页正文结构的地址上入睡。进程醒来时页若有效，页面换入即告完成；否则页服务必须从头再来，因为页的状态可能已经变了——进程可能被挂起任意久，甚至可能被换出过。
4. 页可能不在内存。页若被 `pageout()` 释放后另作他用，可能已被重用。对这类页，页被重新分配时 PTE 中的页号已置零。

缺失的页若是正文页，它可能仍在内存中，就在 5.4 节描述的正文页缓存里。页在内存的时机有两种：PTE 标为按需填充而页是上一次运行留下的；正文页在当前进程挂接正文时已被换出。后一种情形发生在用户释放正文之后——映像和页表被换出保存为粘滞正文（sticky text）。4.3BSD 保存所有正文映像以备重用，此前的 UNIX 版本则靠可执行文件模式中的保存正文位（粘滞位）来保存映像。新进程挂接正文、重装页表时，有些页仍在内存，尽管页表中没有引用。这些页靠在空闲表中搜索与已分配交换空间块号关联的页来定位。页找到后标记为 gone，因为正文页表不再持有对它的引用。要回收该页，`pagein()` 只须把它从空闲表摘下、把页号放进 PTE、置表项有效。这个 PTE 还必须分发到共享该页的其他进程。

故障页若已不在内存，必须分配新页。`pagein()` 不能用惯常的内存分配机制，因为进程一旦阻塞，页的状态就可能改变（导致缺页服务重头再来）。于是 `pagein()` 显式检查内存是否够用：有空闲内存就分配所需的页簇；否则在全局变量 `freemem` 上入睡等内存，然后再重做。

为被换出的页分配好内存后，余下的工作是从交换空间取回页的内容。与 `pagein()` 从文件系统填充或换出页时一样，相邻页也考虑搭车传输。这里，同样被换出的相邻页有资格搭车，只是考虑的块更小（正文页通常 4 KB，数据页 8 KB），因为数据引用的局部性被假定大于正文引用的局部性。

被换出的页若是正文页，共享该正文映像的其他进程必须能察觉换出已经开始。因此，页号填入后，改动过的 PTE 会传播给其他进程，让它们能找到新页的核心映射表项。

内存分配器返回锁定状态的核心映射表项；`pagein()` 把页标记为转移中。页读入后 PTE 置为有效。正文页的 PTE 再次分发使所有副本有效，等待该页的进程被唤醒。因成组而预调的页不置有效：正文页留着可回收，数据或栈页释放但留在空闲表上可回收。这些页若很快被用到，就会在内存中找到；否则等它们排到空闲表头还没被用，就被重新分配。


---

# 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.11.-qing-qiu-diao-ye.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.
