进入系统章节前:先读接口与位操作

完成完整G0后,先学SYS-00的固定宽度、位标志、字节布局与接口返回值,再阅读本页程序。

进入SYS-00系统入口 →

CHAPTER 24 / Linux 与并发系统

虚拟内存、页表与 mmap

一块连续的虚拟地址,为什么不要求连续的物理内存?

阅读与推演约 60 分钟练习时间另计

这一章要弄清楚

  • 拆分页号与页内偏移
  • 区分地址映射、驻留和访问权限
  • 正确建立并释放 mmap 映射

先备知识:指针:地址、空值与有效访问 / 真实资源与RAII:独占拥有 / 进程、系统调用与文件描述符

C++20 / macOS 与 Linux;本章的硬件模型只推演逻辑,不代表设备性能。

地址是一次翻译的输入

在 C++ 里拿到指针后,我们常把它想成 RAM 上的精确位置。对普通用户进程,指针中的地址通常是虚拟地址。CPU 访问它时,需要把虚拟页号翻译成物理页框号,页内偏移保持不变。假设教学机器每页 4096 字节,地址 4099 的虚拟页号是 1、偏移是 3;若页表把虚拟页 1 映到物理页框 7,物理地址就是 7 × 4096 + 3 = 28675。这只是一个小型地址翻译模型,本机页大小必须查询,不能把 4096 当成所有机器的事实。

连续的两个虚拟页可以映到相距很远的物理页框,所以一个大数组在程序看来连续,并不保证 RAM 也连续。反过来,不同进程甚至同一进程的两个地址区间,也可能映射同一个底层物理页。虚拟内存同时提供地址隔离、权限控制、按需分配等能力,并不是“把磁盘当 RAM”这一种功能的别名。

翻译缓存与缺页是两件事

页表描述映射关系;TLB 是最近翻译结果的缓存。TLB miss 表示需要寻找翻译,不必读取磁盘。page fault 表示当前访问不能由已有映射和权限直接完成,需要内核处理。首次访问匿名页可能只需分配一个零页;访问尚未驻留的文件页可能需要 I/O;写只读页可能直接导致进程收到信号。把所有缺页都叫“磁盘慢”会误判性能问题。

访问权限同样属于映射。可读不代表可写;成功分配地址空间也不等于全部页面已经驻留。Linux 上还要理解 overcommit 与实际内存压力:映射成功之后,后续触页仍可能失败或触发系统回收。C++ 对象的有效生命周期又是另一层约束;页可写,不代表任意对象类型都已经在那些字节中合法构造好了。

mmap 建立什么承诺

进入映射例子前,先回到SYS-00 S1:标志组合SYS-00 S3:地址与对象。前者负责位掩码读法,后者只授活int的void*往返及映射三态入口;不能因此认定任意映射字节都是已构造对象,或把20-b整份源码自动记成已准入。

mmap 把一个文件区域或匿名内存区域放入虚拟地址空间。匿名映射适合本章演示,因为它不需要创建或修改真实文件。例子查询页大小,映射两页,在两页交界处分别写入 7 和 9,再读回并 munmap。失败值是 MAP_FAILED,不应只检查空指针。映射长度、访问权限和解除映射范围需要作为同一份资源记录保存。

文件映射的偏移通常需要符合页对齐要求。MAP_PRIVATE 的写入不用于更新原文件,写时复制让修改成为私有版本;MAP_SHARED 则允许共享修改,但可见性、并发同步、持久化是三个问题。另一线程看见字节,不意味着机器断电后也保住了数据。msync、文件系统和存储设备的持久化合同必须单独考虑。

从安全读取到诊断

映射文件后,仍要验证文件长度、字段偏移和整数运算。检查 offset + size 可能先发生溢出,可靠写法是先验证 offset <= length,再比较 size <= length - offset。文件被别的进程截短时,即使之前的范围检查通过,后续映射访问也可能失败;实际解析器需要明确文件稳定性前提。

排查内存问题时先问三个问题:对象还活着吗、虚拟范围允许访问吗、数据并发协议成立吗?ASan 可以发现很多越界与释放后使用,但不是所有 mmap、权限或多线程问题都能靠一次 sanitizer 运行排除。例子的检查只证明地址算式与本机匿名映射行为,不能证明一个真实大文件解析器已经安全。

观察 · 推演

地址 4099 的翻译

虚拟地址4099页号1偏移301 / 03 · MEMORY虚拟地址4099页号1偏移301 / 03 · MEMORY
拆分地址

4099 = 1×4096 + 3。

1 / 3
阅读完整推演文字
  1. 拆分地址

    虚拟地址:4099;页号:1;偏移:3

    4099 = 1×4096 + 3。

  2. 查页表

    页号:1;页表[1]:7;偏移:3

    教学映射中虚拟页 1 对应物理页框 7。

  3. 组合地址

    页框起点:28672;偏移:+3;物理地址:28675

    7×4096+3 得到物理地址 28675。

跟着例子,走完一遍

例题 01C++20 · 本机可运行

手算虚拟地址翻译

教学页大小 4096,页表为 [5,7,2],翻译地址 4099 和 8192。

  1. 地址除以页大小得到虚拟页号。
  2. 取模得到页内偏移。
  3. 查页表并重新组合物理地址,先检查越界。
20-a.cpp
下载
#include <array>
#include <cstddef>
#include <iostream>
#include <stdexcept>
std::size_t translate(std::size_t address) {
    constexpr std::size_t page=4096;
    constexpr std::array<std::size_t,3> frames{5,7,2};
    if(address/page>=frames.size()) throw std::out_of_range("unmapped");
    return frames[address/page]*page+address%page;
}
int main() {
    if(translate(0)!=20480 || translate(4095)!=24575 || translate(4099)!=28675)
        throw std::runtime_error("translation");
    bool rejected=false;
    try {
        const auto unexpected=translate(12288);
        std::cout<<"unexpected unmapped address="<<unexpected<<'\n';
        return 1;
    } catch(const std::out_of_range&) { rejected=true; }
    if(!rejected) throw std::runtime_error("missing bounds");
    std::cout<<"4099->"<<translate(4099)<<" / 8192->"<<translate(8192)<<'\n';
}

如何编译和运行下载的 .cpp 文件 →

结果与解释

4099->28675 / 8192->8192

页内偏移不变,虚拟页号被页框号替换。

在本机运行这个例子

下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 20-a.cpp -o example && ./example

预期标准输出:

4099->28675 / 8192->8192
例题 02POSIX · 需对应系统

在两页交界处读写匿名映射

查询本机页大小,在第 1 页最后一个字节和第 2 页第一个字节写入 7、9。

  1. 用 sysconf 查询页大小,不假设 4 KiB。
  2. mmap 两页并检查 MAP_FAILED。
  3. 读回交界处数据,解除整个映射。
20-b.cpp
下载
#include <sys/mman.h>
#include <unistd.h>
#include <cstddef>
#include <iostream>
#include <limits>
#include <stdexcept>
int main() {
    const long queried=::sysconf(_SC_PAGESIZE);
    if(queried<=0) throw std::runtime_error("page size");
    const auto page=static_cast<std::size_t>(queried);
    if(page>std::numeric_limits<std::size_t>::max()/2) throw std::runtime_error("size");
    void* region=::mmap(nullptr,page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,-1,0);
    if(region==MAP_FAILED) throw std::runtime_error("mmap");
    auto* bytes=static_cast<unsigned char*>(region);
    bytes[page-1]=7; bytes[page]=9;
    const int first=bytes[page-1],second=bytes[page];
    if(::munmap(region,page*2)!=0) throw std::runtime_error("munmap");
    if(first!=7 || second!=9) throw std::runtime_error("boundary");
    std::cout<<"boundary="<<first<<','<<second<<'\n';
}
结果与解释

boundary=7,9

连续虚拟地址让跨页读取保持简单;这不显示实际物理页布局。

在本机运行这个例子

下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 20-b.cpp -o example && ./example

预期标准输出:

boundary=7,9
常见错误

把 mmap 成功当作所有字节随时有效

未检查文件长度就读取字段,或 munmap 后继续使用旧指针。

修正思路:保存映射长度并验证每个字段范围;明确文件不能同时截短;用拥有映射的 RAII 对象控制指针有效期。

轮到你动手

先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。

练习 1

页大小 1024,页表 [9,4]。翻译地址 1023、1024、2048。

给我一点提示
  1. 分别计算商与余数。
  2. 页号必须落在表内。
查看答案与推理

1023 位于页 0、偏移 1023,结果 9×1024+1023=10239;1024 位于页 1,结果 4096;2048 位于页 2,没有映射,应拒绝。

练习 2

文件映射长度 100,检查从偏移 96 读取 8 字节;再考虑 offset 接近 size_t 最大值。

给我一点提示
  1. 先检查 offset 是否大于 length。
  2. 减法比较能避免先加后溢出。
查看答案与推理

96 合法但剩余只有 4,因此拒绝读取 8 字节。统一条件先检查 offset<=length,再检查 size<=length-offset。不要使用可能回绕的 offset+size<=length。

把理解说出来

先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。

解释

虚拟地址连续是否代表物理连续?

参考回答 / English answer

不代表。每个虚拟页独立映射到物理页框,页内偏移才保持不变。

Contiguous virtual pages can map to noncontiguous physical frames. Translation preserves the offset within each page.
解释

TLB miss 与 page fault 是同一回事吗?

参考回答 / English answer

TLB miss 是翻译缓存未命中,可通过页表遍历解决;page fault 需要内核处理当前映射或权限问题,不一定做磁盘 I/O。

A TLB miss is a translation-cache miss. A page fault requires operating-system handling and does not necessarily involve disk I/O.
找错

mmap 失败时检查指针是否等于 nullptr 够吗?

参考回答 / English answer

不够,应比较 MAP_FAILED,并查看 errno;成功的映射地址不要按普通 new 的规则推断。

Mmap reports failure with MAP_FAILED. Checking only for a null pointer uses the wrong API contract.
预测

MAP_PRIVATE 映射中写入文件字节,会更新原文件吗?

参考回答 / English answer

私有写入不用于更新原文件;写时复制建立私有修改。不要把这一点扩展成所有可见性与持久化问题都已解决。

Private mapping writes are not written back to the underlying file. Copy-on-write separates the modified private data.
追问

mmap 成功为何还可能在首次访问时变慢?

参考回答 / English answer

页可能尚未驻留,需要分配、清零或读入;TLB 也可能冷。地址空间保留与实际触页不是同一步。

Mapping establishes address-space state. First access may still require page allocation, initialization, or file I/O.
找错

字段范围检查 offset+size<=length 哪里不可靠?

参考回答 / English answer

加法可能溢出回绕。先约束 offset<=length,再验证 size<=length-offset,并考虑并发截短文件。

The addition can overflow before the comparison. I validate the offset first and compare the size against the remaining length.

继续查证

公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。

接着看已有的图解

这些资料按主题补充本章内容。