虚拟内存 - 理论讲解
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│
└────────────────────┴───┴───┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
关键标志位:
| 位 | 名称 | 说明 |
|---|---|---|
| 0 | P (Present) | 1=页面在内存中 |
| 1 | RW | 0=只读, 1=可读写 |
| 2 | US | 0=仅内核, 1=用户可访问 |
| 5 | A (Accessed) | CPU自动设置(已访问) |
| 6 | D (Dirty) | CPU自动设置(已写入) |
3. 启用分页
步骤
- 创建页目录(4KB对齐的1024项数组)
- 创建页表并填充映射
- 将页目录物理地址写入CR3
- 设置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)
本教程为简化起见使用恒等映射,高半内核留作练习。
下一步
有了虚拟内存,我们可以为每个进程提供独立的地址空间。下一章将实现进程管理!