内存管理与虚拟内存
内存管理是操作系统最核心的职责之一:它负责把有限的物理内存分给多个进程,并保证进程间互不干扰。面试围绕它的提问几乎必考——分页、页表、页面置换、虚拟内存,理解这套机制,也就理解了为什么"程序能用到比物理内存更大的空间"、为什么"访问内存会突然卡顿"。
从连续分配到分页
早期系统采用连续分配:把内存切成固定或动态分区,一个进程独占一整块。它实现简单,但会产生外部碎片,且难以利用零散的小空间。
连续分配: [进程A][空洞][进程B][空洞][进程C] ← 大量外部碎片
分页分配: 物理内存被切成等大小的"页框",页可落任意页框
分页把逻辑地址空间切成固定大小的页(如 4KB),物理内存切成同样大小的页框,页表负责记录"逻辑页 → 物理页框"的映射。每个页可以落在任意空闲页框,外部碎片被彻底消除,最多只剩不到一页的内部碎片。
页表与地址转换
32 位系统、页大小 4KB 时,逻辑地址可拆成"页号 + 页内偏移"(页号占高 20 位,偏移占低 12 位):
逻辑地址 0x1234_5678
→ 页号 0x12345,页内偏移 0x678
→ 查页表得页框号 0xA8
→ 物理地址 = 0xA8 × 0x1000 + 0x678
每访问一页都要查位于内存中的页表,太慢,所以 CPU 引入 TLB(快表) 缓存最近用到的页表项。命中 TLB 时一次访存完成;未命中才去内存查页表,并回填 TLB。
虚拟内存与缺页
虚拟内存让每个进程拥有独立的、远大于物理内存的地址空间。程序运行时只把真正用到的页装入内存,这称为按需分页:
访问页 P → 页表项有效位 = 0 → 触发缺页中断 → 从磁盘换入该页
→ 更新页表 → 重新执行刚才的指令
# 模拟一次缺页处理(内存最多放 3 页,近似 FIFO 置换)
def page_fault(page, memory):
if len(memory) == 3:
memory.pop(0) # 换出最早进入的页
memory.append(page)
print("缺页后内存:", memory) # 输出:缺页后内存: [2, 3, 4]
memory = [1, 2, 3]
page_fault(4, memory)
缺页要从磁盘读页,速度相差几十万倍,缺页率直接决定程序性能。常见置换算法有 FIFO(先进先出)、LRU(最近最久未使用)、OPT(理论最优,不可实现);其中 LRU 最常被要求手写,标准实现是"哈希表 + 双向链表"。注意 FIFO 存在 Belady 异常(增大页框数缺页反而更多),LRU 不会。
分段与段页式
分段按程序逻辑划分(代码段、数据段、栈段),便于共享与保护,但会产生外部碎片。段页式先分段、段内再分页,兼顾两者,现代操作系统以分页为基石。分段的逻辑地址是"段号 + 段内偏移",段表记录段的基址与长度,访问越界时触发保护异常;分页对程序员透明,分段则可见。
小结
内存管理的主线是把"逻辑地址"翻译成"物理地址":分页解决碎片,虚拟内存 + 按需分页让内存"用起来更大",TLB 与 LRU 是性能关键。面试时能画出页表转换、讲清缺页流程,这一题就稳了。