点亮什么、为什么、设计图
这一章我们要点亮什么
核心是一件:把虚拟地址空间变成一等公民——一个可以创建、切换、销毁的对象 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 位单独看:
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,内核半区共享、用户半区私有」。
内核 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,所以内核映射在所有空间共享;用户半区各自独立的下级页表。生命周期是这五步:
启动: 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] 一律不动(共享, 归内核)