虚拟内存 - 理论讲解

1. 为什么需要虚拟内存?

没有虚拟内存的问题

  • 地址冲突: 多个程序可能使用相同的地址
  • 内存保护缺失: 一个程序可以修改另一个程序的内存
  • 碎片化: 进程退出后留下不连续的空闲区域
  • 内存不够用: 物理内存有限

虚拟内存的好处

好处说明
隔离每个进程有独立的4GB地址空间
保护页表项标记用户/内核权限
灵活映射不连续物理帧可映射到连续虚拟地址
交换(Swap)可将不常用页换出到磁盘

2. x86 分页机制

地址翻译过程

           虚拟地址: 0x12345678
           ┌───────────────────────────────┐
           │0000010010│0011010101│011001111000│
           │ PD Index │ PT Index │  Offset   │
           │  = 0x48  │  = 0x345 │  = 0x678  │
           └──────┬───┴────┬─────┴─────┬─────┘
                  │        │           │
     CR3──→ ┌────┴────┐   │           │
             │页目录    │←─┘           │
             │[0x48]───┼──→┌─────────┐│
             │         │   │页表      ││
             └─────────┘   │[0x345]──┼┼──→ 物理帧基址
                           │         ││      + 0x678
                           └─────────┘│    = 物理地址
                                      │

页目录/页表条目格式

31                    12  11  9  8  7  6  5  4  3  2  1  0
┌────────────────────┬───┬───┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│   物理帧地址(20位) │AVL│ 0 │ 0│PS│ D│ A│CD│WT│US│RW│ P│
└────────────────────┴───┴───┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

关键标志位:

位名称说明
0P (Present)1=页面在内存中
1RW0=只读, 1=可读写
2US0=仅内核, 1=用户可访问
5A (Accessed)CPU自动设置(已访问)
6D (Dirty)CPU自动设置(已写入)

3. 启用分页

步骤

  1. 创建页目录(4KB对齐的1024项数组)
  2. 创建页表并填充映射
  3. 将页目录物理地址写入CR3
  4. 设置CR0的PG位(bit 31)
void vmm_enable_paging(page_directory_t *dir) {
    // 加载页目录到CR3
    __asm__ volatile("mov %0, %%cr3" : : "r"(dir->physical_addr));
 
    // 启用分页:设置CR0的PG位
    uint32_t cr0;
    __asm__ volatile("mov %%cr0, %0" : "=r"(cr0));
    cr0 |= 0x80000000;
    __asm__ volatile("mov %0, %%cr0" : : "r"(cr0));
}

4. 恒等映射(Identity Mapping)

启用分页后,CPU将所有地址视为虚拟地址。为了不让当前执行的代码”消失”,至少需要将内核区域进行恒等映射:

虚拟地址 0x00000000-0x003FFFFF → 物理地址 0x00000000-0x003FFFFF
// 映射前4MB(1024个页帧)
for (uint32_t i = 0; i < 1024; i++) {
    page_table[i] = (i * PAGE_SIZE) | PAGE_PRESENT | PAGE_WRITABLE;
}
page_directory[0] = (uint32_t)page_table | PAGE_PRESENT | PAGE_WRITABLE;

5. 缺页异常处理

当访问未映射或权限不足的虚拟地址时,CPU产生 Page Fault (INT 14)。

错误码含义

位0: P  - 0=页面不存在, 1=权限错误
位1: W  - 0=读操作, 1=写操作引起
位2: U  - 0=内核模式, 1=用户模式引起

CR2寄存器

缺页异常发生时,CPU将引起异常的虚拟地址存入CR2:

void page_fault_handler(registers_t *regs) {
    uint32_t faulting_addr;
    __asm__ volatile("mov %%cr2, %0" : "=r"(faulting_addr));
 
    bool present  = regs->err_code & 0x1;
    bool write    = regs->err_code & 0x2;
    bool user     = regs->err_code & 0x4;
 
    screen_printf("Page fault at 0x%x [%s %s %s]\n",
        faulting_addr,
        present ? "protection" : "not-present",
        write ? "write" : "read",
        user ? "user" : "kernel");
}

6. 内核映射方案

高半内核(Higher Half Kernel)

常见做法是将内核映射到虚拟地址的高半部分(如3GB处):

虚拟地址空间(4GB):
0x00000000 ─ 0xBFFFFFFF  → 用户空间 (3GB)
0xC0000000 ─ 0xFFFFFFFF  → 内核空间 (1GB)

本教程为简化起见使用恒等映射,高半内核留作练习。

下一步

有了虚拟内存,我们可以为每个进程提供独立的地址空间。下一章将实现进程管理!