从GeekOS实验报告到实战:手把手教你实现一个简易的虚拟内存分页系统(附完整代码)

张开发
2026/5/17 13:16:44 15 分钟阅读
从GeekOS实验报告到实战:手把手教你实现一个简易的虚拟内存分页系统(附完整代码)
从GeekOS实验到实战构建轻量级虚拟内存分页系统全指南1. 虚拟内存分页系统的核心价值在计算机科学领域虚拟内存技术堪称操作系统设计的魔法——它让有限物理内存运行远超其容量的程序成为可能。这项诞生于1960年代的技术至今仍是现代操作系统的基石。通过分页机制操作系统为每个进程创造了一个独立的地址空间幻觉让程序员无需关心物理内存的实际分配情况。传统实验报告往往聚焦理论而轻实践导致学习者即使理解了分页原理面对实际编码时仍束手无策。本文将以GeekOS Project4为蓝本但完全跳脱实验报告的框架从工程角度重新设计一个教学用虚拟内存系统。我们将重点关注地址转换的硬件协同如何利用x86架构的CR3寄存器、页目录和页表缺页中断的完整处理流程从异常触发到页面换入的全过程内存置换策略的实现技巧时钟算法的优化版本及其实现细节与学术实验不同我们的实现将强调工业级编码实践包括// 示例页表项标志位设置宏 #define PAGE_PRESENT 0x01 #define PAGE_WRITABLE 0x02 #define PAGE_USER 0x04 #define PAGE_ACCESSED 0x20 #define PAGE_DIRTY 0x402. 系统架构设计2.1 两级页表结构x86架构采用经典的两级页表设计这是32位时代的最佳平衡点。我们的实现需要精确控制以下数据结构结构类型大小描述页目录4KB包含1024个页目录项(PDE)页表4KB每个页表含1024个页表项(PTE)页框4KB实际物理内存块关键转换函数实现uint32_t virt_to_phys(uint32_t virt_addr, uint32_t* page_dir) { uint32_t dir_index (virt_addr 22) 0x3FF; uint32_t table_index (virt_addr 12) 0x3FF; if (!(page_dir[dir_index] PAGE_PRESENT)) return 0; // 页目录项不存在 uint32_t* page_table (uint32_t*)(page_dir[dir_index] 0xFFFFF000); if (!(page_table[table_index] PAGE_PRESENT)) return 0; // 页表项不存在 return (page_table[table_index] 0xFFFFF000) | (virt_addr 0xFFF); }2.2 内存布局规划合理的地址空间划分是系统稳定的前提。我们的设计采用以下布局0x00000000-0x003FFFFF用户空间4MB0xC0000000-0xFFFFFFFF内核空间1GB0xFFC00000-0xFFFFFFFF内核页表映射区4MB注意内核空间固定在3GB以上是Linux的经典设计可避免用户态直接访问内核数据3. 核心组件实现3.1 页表初始化系统启动时需要建立初始页表关键步骤包括分配页目录和初始页表物理页映射内核代码和数据区设置特殊区域如APIC加载CR3寄存器并启用分页典型问题解决方案APIC处理需要单独映射0xFEE00000区域页表项标志位内核页面应设置PAGE_WRITABLE用户页面需加PAGE_USERvoid init_paging() { // 分配页目录必须4KB对齐 uint32_t* page_dir alloc_aligned_page(); memset(page_dir, 0, PAGE_SIZE); // 映射内核1MB区域 for (uint32_t i 0; i 256; i) { uint32_t virt 0xC0000000 i * PAGE_SIZE; uint32_t phys i * PAGE_SIZE; map_page(page_dir, virt, phys, PAGE_PRESENT|PAGE_WRITABLE); } // 启用分页 asm volatile(mov %0, %%cr3 : : r (page_dir)); asm volatile(mov %%cr0, %%eax; orl $0x80000000, %%eax; mov %%eax, %%cr0 ::: eax); }3.2 缺页中断处理缺页中断#PF是虚拟内存系统的核心事件处理流程如下通过CR2寄存器获取故障地址分析错误码确定故障类型根据不同类型采取相应措施写时复制(COW)按需分页权限检查优化后的处理函数框架void page_fault_handler(registers_t* regs) { uint32_t fault_addr; asm volatile(mov %%cr2, %0 : r (fault_addr)); int present !(regs-err_code 0x1); // 页面不存在 int write regs-err_code 0x2; // 写操作 int user regs-err_code 0x4; // 用户模式 if (user !(current-page_dir[fault_addr22] PAGE_USER)) { kill_process(current); // 非法访问 return; } if (present) { handle_page_missing(fault_addr); } else if (write) { handle_write_protect(fault_addr); } // 更新统计信息 current-page_faults; }4. 高级优化技巧4.1 时钟页面置换算法相比实验报告中的基础LRU我们实现更高效的时钟算法struct page { uint32_t flags; uint32_t counter; // 其他字段... }; struct page* find_victim_page() { static uint32_t clock_hand 0; while (true) { struct page* pg page_array[clock_hand]; if (pg-flags PAGE_ACCESSED) { pg-flags ~PAGE_ACCESSED; pg-counter 0; } else { if (pg-counter AGE_THRESHOLD) { return pg; } } clock_hand (clock_hand 1) % NUM_PAGES; } }4.2 写时复制优化通过共享物理页减少内存占用int copy_on_write(uint32_t virt_addr) { uint32_t phys virt_to_phys(virt_addr, current-page_dir); if (page_refcount[phys] 1) { // 唯一引用直接修改权限 set_page_flags(virt_addr, PAGE_WRITABLE); return 0; } // 需要复制页面 uint32_t new_phys alloc_page(); memcpy((void*)new_phys, (void*)phys, PAGE_SIZE); map_page(current-page_dir, virt_addr, new_phys, PAGE_PRESENT|PAGE_WRITABLE|PAGE_USER); atomic_dec(page_refcount[phys]); page_refcount[new_phys] 1; return 1; }5. 调试与测试策略5.1 单元测试框架构建专门的内存测试模块void test_paging() { // 测试基础映射 void* ptr vmalloc(4096); assert(ptr ! NULL); *(int*)ptr 0x12345678; assert(*(int*)ptr 0x12345678); // 测试缺页处理 uint32_t* big_array vmalloc(1024*1024); // 1MB for (int i 0; i 256*1024; i) { big_array[i] i; // 触发缺页 } // 测试权限 bool fault_triggered false; set_catch_page_fault(true); __try { *(int*)0x00000000 0; // 访问NULL指针 } __except { fault_triggered true; } assert(fault_triggered); }5.2 性能分析技巧关键指标监控方法指标监控方法优化方向缺页率统计#PF次数调整工作集置换频率记录swap次数优化算法TLB命中率性能计数器大页支持实际项目中我们发现调整页面置换算法的年龄阈值(AGE_THRESHOLD)对性能影响显著。通过基准测试确定最优值# 测试脚本示例 for age in 3 5 7 10; do echo Testing AGE_THRESHOLD$age sed -i s/#define AGE_THRESHOLD .*/#define AGE_THRESHOLD $age/ paging.c make run_benchmark done6. 工程实践建议6.1 常见陷阱规避TLB刷新遗漏修改页表后必须调用invlpg指令递归页表映射页目录最后一个条目应指向自身原子性保证页面分配/释放需要关闭中断提示x86架构下mov cr3指令会自动刷新TLB但局部更新时仍需invlpg6.2 扩展思路大页支持使用2MB/4MB页减少TLB压力内存压缩在swap前尝试压缩页面内容NUMA感知多核系统中考虑内存位置进阶数据结构示例多级年龄计数struct page { uint8_t age : 4; // 4位年龄计数器 uint8_t active : 1; // 活跃标志 // ... }; void update_page_age() { for (int i 0; i NUM_PAGES; i) { if (pages[i].active) { if (pages[i].age 15) pages[i].age; } else { if (pages[i].age 0) pages[i].age--; } pages[i].active 0; } }在云计算环境中我们曾将这套机制扩展为分布式虚拟内存系统通过RDMA协议实现跨节点的页面交换将单机内存管理扩展为集群级资源池。这种设计允许容器突破物理内存限制同时保持本地访问性能——当页面被频繁访问时它会自动迁移到本地节点。

更多文章