系统调用与分页内存模拟
本节目标
- 理解虚拟内存与分页(page)的基本思想
- 手写意分页:用页表把"虚拟地址"翻译为"物理地址"
- 串联"用户程序 → syscall → 内核 → 硬件"全链路
分页:内存被切成固定大小的"页"(如 4KB),程序用"虚拟地址",由页表翻译成"物理地址"。好处:隔离、按需分配、可换出。
// 运行环境:Node.js 14+
// 保存为 aj-l13.js,执行:node aj-l13.js
const PAGE = 4096
class MMU {
constructor() {
this.pageTable = new Map() // 虚拟页号 -> 物理页框号
this.physFrames = [] // 物理内存(用数组模拟页框)
}
allocPage(vpn) {
const frame = this.physFrames.length
this.physFrames.push(Buffer.alloc(PAGE))
this.pageTable.set(vpn, frame)
return frame
}
// 地址翻译:虚拟地址 = 虚拟页号 * PAGE + 页内偏移
translate(vaddr) {
const vpn = Math.floor(vaddr / PAGE)
const offset = vaddr % PAGE
const frame = this.pageTable.get(vpn)
if (frame === undefined) throw new Error('缺页(page fault),需分配')
return frame * PAGE + offset // 物理地址
}
}
// 调用示例
const mmu = new MMU()
mmu.allocPage(0) // 虚拟页 0 映射到物理框 0
console.log('虚拟 5000 -> 物理', mmu.translate(5000)) // 5000 落在第1页(offset 904)缺页(page fault):访问的虚拟页还没映射物理页,触发异常,内核分配一页再重试。这是"按需分页"让程序"以为"有无限内存"的魔法。
名词解释
- 虚拟内存(Virtual Memory):每个进程看到的独立连续地址空间,由 MMU 经页表映射到物理内存。进程彼此隔离,且可使用超过物理内存的容量(配合换出)。
- 页(Page)/ 页框(Frame):虚拟侧叫页,物理侧叫页框,大小一致(常 4KB)。页表记录"第几页 → 第几框"。
- 缺页异常(Page Fault):访问的虚拟页尚未映射物理页时触发的异常;内核据此分配物理页并建立映射后重试指令。
课后练习
- 为什么要用虚拟地址而不是直接用物理地址?
- 答案:虚拟地址让每个进程有独立空间、互不踩踏,且物理内存可灵活分配/换出;若直接用物理地址,多进程会互相覆盖、且碎片难管理。
translate(5000)为什么落在"第 1 页"?- 答案:页大小 4096,5000 = 1×4096 + 904,所以虚拟页号=1、页内偏移=904,经页表查出物理框号后拼成物理地址。
总结
分页内存模拟,是打通"用户程序如何安全使用内存"这最后一环。你会发现,程序里写的每个指针地址都是"虚拟"的,真正落地要靠 MMU 这张页表翻译成物理地址——这正是多进程互不干扰、还能"超额使用内存"的秘密。缺页异常更是神来之笔:它让程序以为自己拥有连续大内存,实际物理页是"用到才给",用换出机制把不常用的页暂存磁盘。我特别想强调这个设计的工程美感:用一层间接(页表)同时换来了隔离性、灵活性和效率。理解虚拟内存,你看 OOM(内存耗尽其实是虚拟页分配失败)、看swap分区、看浏览器多标签页隔离,都明白它们共享同一套"地址翻译"底层。从引导、中断、调度到分页,你今天串联起了操作系统全景——它不再是课本名词,而是你已经亲手跑过的状态机与映射表。