系统调用与分页内存模拟

本节目标

分页:内存被切成固定大小的"页"(如 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):访问的虚拟页还没映射物理页,触发异常,内核分配一页再重试。这是"按需分页"让程序"以为"有无限内存"的魔法。

名词解释

课后练习

  1. 为什么要用虚拟地址而不是直接用物理地址?
    • 答案:虚拟地址让每个进程有独立空间、互不踩踏,且物理内存可灵活分配/换出;若直接用物理地址,多进程会互相覆盖、且碎片难管理。
  2. translate(5000) 为什么落在"第 1 页"?
    • 答案:页大小 4096,5000 = 1×4096 + 904,所以虚拟页号=1、页内偏移=904,经页表查出物理框号后拼成物理地址。

总结

分页内存模拟,是打通"用户程序如何安全使用内存"这最后一环。你会发现,程序里写的每个指针地址都是"虚拟"的,真正落地要靠 MMU 这张页表翻译成物理地址——这正是多进程互不干扰、还能"超额使用内存"的秘密。缺页异常更是神来之笔:它让程序以为自己拥有连续大内存,实际物理页是"用到才给",用换出机制把不常用的页暂存磁盘。我特别想强调这个设计的工程美感:用一层间接(页表)同时换来了隔离性、灵活性和效率。理解虚拟内存,你看 OOM(内存耗尽其实是虚拟页分配失败)、看swap分区、看浏览器多标签页隔离,都明白它们共享同一套"地址翻译"底层。从引导、中断、调度到分页,你今天串联起了操作系统全景——它不再是课本名词,而是你已经亲手跑过的状态机与映射表。