C++ / a working model

127 / 163   ·   C11   ·   约 8 分钟

完整虚拟内存系统

先记住这句话

本章通过VAX/VMS和Linux两个实例,说明如何将页表设计、TLB管理、页面替换以及其他性能、安全与功能特性组合成一个能在从嵌入式设备到超级计算机上运行的完整虚拟内存系统。

本篇内容
  1. 完整虚拟内存系统要解决的问题
  2. VAX的小页面与页表压缩方法
  3. 空指针检测与内核共享映射
  4. 运行示例
  5. 动手练习

官方章节 PDF

完整虚拟内存系统要解决的问题

页表、TLB和替换算法只是基础。完整系统还得适应差异巨大的硬件、方便内核访问用户数据、提供调试支持并保证安全隔离。早期的VAX/VMS已经用软件技巧弥补硬件缺陷,这些思路至今仍在Linux等系统中沿用。

VAX的小页面与页表压缩方法

512字节的页面让每个进程的线性页表 theoretically 可达数千万项。VAX把32位空间分成P0、P1和系统段,只用两套用户页表覆盖实际使用的区域。更妙的是把这两套页表放在内核自己的虚拟地址空间里,内存紧张时可以把页表页换出去。

空指针检测与内核共享映射

地址空间的第0页被标记为无效,空指针立刻产生故障。内核代码和数据出现在每个进程的高地址,切换进程时系统段寄存器保持不变。硬件保护位确保用户程序无法读写内核页,内核因此可以像一个受保护的共享库一样工作。

常见误区

  • 低估小页面带来的页表体积,导致内存被页表本身耗尽
  • 不把内核映射进用户空间,系统调用时拷贝用户缓冲区会变得极其复杂
  • 漏掉第0页的无效标记,空指针错误会静默破坏内存而不是立即被发现

运行一个例子

最低标准 C11 · 完整程序 · 下载 .c

#include <stdio.h>
#include <stdint.h>

int main(void) {
    printf("VAX-like 32-bit address space simulation (512-byte pages)\n");
    printf("VPN = VA >> 9 (23 bits); segment from top 2 bits of VPN\n\n");
    const uint32_t examples[7] = {
        0x00000000u,
        0x00001000u,
        0x3FFFFFFFu,
        0x40000000u,
        0x7FFFFFFFu,
        0x80000000u,
        0xC0000000u
    };
    const char *names[7] = {
        "null page",
        "user code",
        "end of P0 heap",
        "start of P1 stack",
        "end of P1",
        "kernel start",
        "unused"
    };
    for (int i = 0; i < 7; i++) {
        uint32_t va = examples[i];
        uint32_t offset = va & 0x1FFu;
        uint32_t vpn = va >> 9;
        uint32_t seg = (vpn >> 21) & 3u;
        const char *segname;
        switch (seg) {
            case 0: segname = "P0 (process)"; break;
            case 1: segname = "P1 (process)"; break;
            case 2: segname = "S (system)"; break;
            default: segname = "unused"; break;
        }
        printf("VA=0x%08X  VPN=%7u  off=%3u  seg=%s  [%s]%s\n",
               va, vpn, offset, segname, names[i],
               (va < 512 ? " INVALID" : ""));
    }
    printf("\nNote: page 0 is marked invalid to catch null-pointer bugs.\n");
    printf("Kernel (S) is mapped in every process for easy pointer passing.\n");
    return 0;
}

在本地编译

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-23-complete-vm.c -o example && ./example

预期结果

VAX-like 32-bit address space simulation (512-byte pages)
VPN = VA >> 9 (23 bits); segment from top 2 bits of VPN

VA=0x00000000  VPN=      0  off=  0  seg=P0 (process)  [null page] INVALID
VA=0x00001000  VPN=      8  off=  0  seg=P0 (process)  [user code]
VA=0x3FFFFFFF  VPN=2097151  off=511  seg=P0 (process)  [end of P0 heap]
VA=0x40000000  VPN=2097152  off=  0  seg=P1 (process)  [start of P1 stack]
VA=0x7FFFFFFF  VPN=4194303  off=511  seg=P1 (process)  [end of P1]
VA=0x80000000  VPN=4194304  off=  0  seg=S (system)  [kernel start]
VA=0xC0000000  VPN=6291456  off=  0  seg=unused  [unused]

Note: page 0 is marked invalid to catch null-pointer bugs.
Kernel (S) is mapped in every process for easy pointer passing.

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

为什么VAX要把用户页表放在内核虚拟内存中而不是物理内存?这样做有什么额外好处?

查看参考答案

这样页表本身也可以被换出到磁盘,在内存压力大时释放物理帧给真正的用户数据。同时内核可以用普通的虚拟地址访问这些页表,简化了实现。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录