Skip to content

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

这一章我们要点亮什么

两件事,一件是能力,一件是惊喜。

第一件,VMM 给了内核主动控制虚拟→物理映射的能力。map(virt, phys, flags) 把一个物理页挂到某个虚拟地址;unmap 拆掉;translate 查一个虚拟地址实际落在哪个物理页。映射时如果中间的页表还不存在,它会自动从 PMM 要页、建表、链接——你不用预先把四级页表全建好。013 那个硬编码页表地址的 map_mmio hack,终于有了一套正经的替代能力。

第二件,稍微超出预期:我们在缺页异常(#PF)处理里接上了 VMM,实现了 demand paging(按需调页)。往后,凡是访问一个「还没映射」的虚拟页,CPU 触发缺页异常,我们的 handler 现场从 PMM 分配一页、用 VMM 映射上去、然后返回——那条指令重新执行,这次就成功了。于是内核不用预先为所有可能用到的地址建映射,「用到才给」。

这两件事合起来,意味着内核从「跑在一套写死的页表上」升级成了「自己管理虚拟地址空间」。这是后面进程、堆、独立地址空间的真正前提。

为什么现在需要它

先说为什么紧跟 015。015 让我们能分配物理页了,但物理页拿到手只是个物理地址,「把它挂到哪个虚拟地址」还没法控制。换句话说,PMM 解决了「物理页从哪来」,但没解决「物理页怎么出现在虚拟地址空间里」。这两件事必须配套:有 PMM 没 VMM,你拿到的物理页用不上(除非它正好已经在某个固定映射里);有 VMM 没 PMM,VMM 建中间页表时没处要页。所以 015 一就位,016 紧跟着就来,它俩是内存子系统的一对基石。

再回头看那个 map_mmio。013 为了点亮屏幕,我们写了个最小的页表助手:它硬编码了两个页表虚拟地址、往里塞大页条目、只做恒等映射、不管权限、不管回收。当时就说了「这是个 hack,等做了正经页表管理器就替掉它」。VMM 就是那个正经的管理器:它能走任意路径的 4 级页表、按需建表、精细控制 flag。map_mmio 那种「摸黑改固定地址」的做法,从此有了正经的替代者——不过这一章 framebuffer 那一路暂且还沿用旧的 map_mmio(它还能用,没必要为换而换),VMM 的 map 能力是为更往后的用途(给进程建独立地址空间、动态映射)铺的路,真正替换 framebuffer 那条路径要等后续重构。

还有一笔面向未来的账。VMM 的 map / unmap 都接受一个可选的 pml4 参数——也就是「在哪套页表里做映射」。这一章我们总用内核自己的那套,但这个参数是为以后留的:将来要给每个进程一套独立的页表(独立的虚拟地址空间),map 只要换个 pml4 就能往进程的页表里映射。所以这一章虽然只服务内核自己,但它把「多地址空间」的接口形状先定下来了。

设计图

VMM 的核心操作是「走 4 级页表」。x86-64 的虚拟地址被拆成五段:四级页表的索引各 9 位,加 12 位页内偏移。

text
   虚拟地址 64 位
   ┌─────────┬─────────┬─────────┬─────────┬────────────┐
   │ PML4 idx│ PDPT idx│  PD idx │  PT idx │ page offset│
   │ [39..47]│ [30..38]│ [21..29]│ [12..20]│  [0..11]   │
   └────┬────┴────┬────┴────┬────┴────┬────┴─────┬──────┘
        ▼         ▼         ▼         ▼          ▼
   CR3→PML4 ──→ PDPT ──→  PD ──→  PT  ──→  物理页基址 + offset
   (每级 512 项; 不存在的中间表, map 时从 PMM 现建)

   walk_level(table, index, should_alloc):
        entry = table[index]
        if entry.is_present(): → phys_to_virt(entry.phys_addr())  走下一级
        else if should_alloc:
            new = PMM.alloc_page()         ← 中间表也是一页物理内存
            把 new 清零                       ← 必须清零, 否则陈旧位会被当成 present
            entry = new | PRESENT | WRITABLE (链接进上一级)
            → phys_to_virt(new)
        else: → nullptr  (translate/unmap 走到这里就停)

   phys_to_virt(phys) = phys + KERNEL_VMA   ← 靠高半区偏移访问「物理」页表(自举关键)

demand paging 则是另一条线,挂在缺页异常上:

text
   访问一个未映射的虚拟地址
        ▼ CPU 触发 #PF(vector 14)
   handle_pf:
        fault_addr = CR2              ← CR2 存引发缺页的地址
        err = error_code
        if (err & 0x01) == 0:         ← bit0=0 表示「页不存在」(可补)
            phys = PMM.alloc_page()
            if phys && VMM.map(virt_page, phys, PRESENT|WRITABLE):
                return                 ← 补好了, 重新执行那条指令
        // bit0=1 是 protection violation(真错误), 走诊断 + 挂起

两条线的交汇点就是 VMM:map 是「主动映射」,demand paging 是「被动(缺页时)映射」,两者都调同一个 VMM::map

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