Skip to content

点亮什么、为什么、设计图

这一章我们要点亮什么

核心是一件:把虚拟地址空间变成一等公民——一个可以创建、切换、销毁的对象 AddressSpace

具体说,一个 AddressSpace 实例:

  • 拥有自己的一套页表根(一张独立的 PML4),和内核的、和别的实例的都不一样。
  • 看得见内核:它的 PML4 里,内核那一半(PML4[256..511])是从内核页表拷过来的,所以无论切到哪个地址空间,内核映射始终在——否则一切换就找不到内核代码了。
  • 用户那一半私有(PML4[0..255]):每个空间自己往里映射,互不可见。这正是「进程隔离」的物理基础。
  • 能切换:activate() 把自己的 PML4 写进 CR3,从此 CPU 按这套页表走地址。
  • 能销毁:析构时把用户半区的页表子树(连同里面的数据页)全回收还给 PMM,内核半区不动。

合起来,这一章给了内核「造一个隔离的虚拟世界、钻进去、再拆掉」的能力。016 留的 pml4 参数,这一章真正用上了。

但要把期望放正:这一章交付「地址空间」这个基础设施。它造出来是给谁用的?——给进程。可进程(进程结构、调度、上下文切换)是下一章(019)的事。所以在 018,AddressSpace 在生产路径里其实只做了一件事(存下内核 PML4),实例只活在测试里——它是一块「铺好、测好、等进程来用」的地基。

为什么现在需要它

先解开那个数字。0xFFFF800000000000 看着像拍脑袋选的,其实它是 x86-64 虚拟地址空间里一个有确切含义的位置。回忆 016:虚拟地址的 [39..47] 位是 PML4 的索引(9 位,共 512 项)。把这 9 位单独看:

text
   PML4 索引 = (virt >> 39) & 0x1FF        // 0..511

   0xFFFF800000000000  →  PML4[256]   ← 堆基址(017): 内核半区的第一个表项
   0xFFFFFFFF80000000  →  PML4[511]   ← KERNEL_VMA / phys_to_virt: 内核半区的最后一个表项
   0x0000000020000000  →  PML4[  0]   ← 用户态地址: 用户半区的起点
   0x00007FFFFFFFFFFF  →  PML4[255]   ← 用户态地址: 用户半区的顶

0xFFFF800000000000 的 PML4 索引正好是 256。这不是巧合:x86-64 的虚拟地址是「规范地址(canonical)」,bit 47 是分水岭——bit 47 为 0 是低半区(用户),为 1 是高半区(内核),且高位必须按 bit 47 符号扩展。PML4 索引 256 正是 bit 47 第一次为 1 的那个表项,也就是内核半区的入口。所以 017 把堆放在 0xFFFF800000000000,等于放在「内核半区的第 0 项」——它待在所有地址空间都会共享的那一半里。这就是那个硬编码数字的全部含义:它在内核半区,因此天然在(未来的)每个地址空间里都可见。

这就引出了这一章的动机。到目前为止,内核只有「一套」页表——bootloader / VMM 建的那套,所有代码都在它上面跑。PMM、VMM、堆,全在内核半区。可是将来的进程需要各自的虚拟地址空间:进程 A 看到的 0x400000 不能是进程 B 看到的同一个物理页,否则谈不上隔离。要做到这点,每个进程得有自己的 PML4,自己的用户半区;但内核只有一份,不能给每个进程都拷一份完整的内核映射——那既贵又没必要。

经典的解法就是「共享内核半区」:每个地址空间自己有一张 PML4,其中内核半区那 256 项(PML4[256..511])指向同一套内核下级页表(PDPT/PD/PT),用户半区(PML4[0..255])各自独立。这样内核映射在所有空间里都一致可见(共享),用户映射各自隔离(私有),开销只是一张 PML4(4 KB)加用户半区的页表。AddressSpace 就是这个解法的落地。

所以 018 紧跟 017:017 让内核能动态分配了,018 把「地址空间」这个抽象立起来,为进程铺最后一层地基。016 的 pml4 参数,到这里终于有了调用者。

设计图

AddressSpace 的核心是「每个实例一张独立 PML4,内核半区共享、用户半区私有」。

text
   内核 PML4(init_kernel 时从 CR3 存下)         某个 AddressSpace 实例的 PML4
   ┌──────────────────────┐                      ┌──────────────────────┐
   │ [0]   用户 ...        │  ← 私有, 各实例不同   │ [0]   用户(空, 自己 map)│
   │ ...                   │                      │ ...                   │
   │ [255] 用户顶           │                      │ [255]                 │
   ├──────────────────────┤                      ├──────────────────────┤
   │ [256] ──┐ 内核 PDPT    │   构造时:逐项拷贝     │ [256] ──┐ 指向【同一套】│
   │ ...     │  (heap 等)   │  ────────────────►   │ ...     │  内核下级页表 │
   │ [511] ──┘ 内核镜像/VMA  │   (浅拷贝, 共享引用)  │ [511] ──┘              │
   └──────────────────────┘                      └──────────────────────┘
        ▲                                              ▲
   kernel_pml4_ (static)                         pml4_phys_ (每实例)
   g_vmm 默认用这套                              activate() → 写进 CR3

   关键:PML4[256..511] 是「浅拷贝」——新 PML4 的这些项指向内核那套
   PDPT/PD/PT,所以内核映射在所有空间共享;用户半区各自独立的下级页表。

生命周期是这五步:

text
   启动:  AddressSpace::init_kernel()           ← read_cr3() 存 kernel_pml4_
            (VMM init 之后, 任何 AS 构造之前; main.cpp Step 9)

   构造:  AddressSpace as;
            alloc 一页做 PML4 → 512 项清零 → PML4[256..511] = kernel_pml4_ 的对应项

   使用:  as.map(virt, phys, flags)   → g_vmm.map(..., &as.pml4_phys_)   // 以本空间为根
          as.translate(virt)          → g_vmm.translate(..., &as.pml4_phys_)
          as.activate()               → write_cr3(as.pml4_phys_)          // 切过去

   销毁:  ~AddressSpace()
            遍历 PML4[0..255], 每个 present 项 → free_subtree(回收 PDPT/PD/PT 子树 + 数据页)
            free_page(PML4 本身)
            内核半区[256..511] 一律不动(共享, 归内核)

035_multi_terminal-45-gf25de18 · f25de18 · 2026-08-04