内存管理
物理内存与虚拟内存
- 物理内存:硬件提供的 RAM,容量有限
- 虚拟内存:每个进程拥有独立的虚拟地址空间(如 32 位 4GB,64 位可达 256TB),由 MMU(内存管理单元)将虚拟地址映射到物理地址
虚拟内存的核心价值:
- 隔离:进程间互不干扰
- 简化:每个进程看到连续的地址空间
- 超量使用:物理内存不足时,将不活跃页换出到磁盘(Swap)
分页(Paging)
基本概念
- 页(Page):虚拟地址空间按固定大小划分,通常 4KB
- 页框(Page Frame):物理内存同样大小划分
- 页表(Page Table):虚拟页号 → 物理页框号的映射表
多级页表
64 位地址空间下,单级页表会占用海量内存。现代 CPU 使用多级页表:
- x86-64:4 级页表(PML4 → PDPT → PD → PT),每级 512 项
- ARMv8-A:同样支持 4 级页表
虚拟地址分解(x86-64 4KB 页):
| PML4(9) | PDPT(9) | PD(9) | PT(9) | 页内偏移(12) |多级页表的核心优势:按需创建——只有实际使用的虚存区域才分配页表项。
TLB(Translation Lookaside Buffer)
- CPU 内部的页表硬件缓存
- 缓存最近使用的虚拟地址到物理地址的映射
- 局部性原理:TLB 命中时无需访问内存中的页表
- TLB Miss 需要遍历多级页表,开销极大
大页(Huge Pages):使用 2MB 或 1GB 的大页可减少 TLB Miss,常用于数据库、JVM 等内存密集型应用。
页面置换算法
物理内存不足时,需要将某些页面换出到磁盘:
| 算法 | 描述 | 评价 |
|---|---|---|
| FIFO | 先入先出 | 简单但可能换出频繁使用的页面(Belady 异常) |
| LRU | 最近最少使用 | 近似最优,但硬件实现成本高 |
| Clock(NRU) | 近似 LRU,使用引用位 | Linux 实际采用的方案 |
| LFU | 最不经常使用 | 可能积累"冷数据"长期不被换出 |
| 最优(OPT) | 置换未来最晚使用的 | 理论最优,无法实现 |
| 工作集 | 基于进程工作集模型 | 避免抖动 |
分段(Segmentation)
概念
进程地址空间按逻辑划分,每个段有独立基址和界限:
- 代码段(Code/Text)
- 数据段(Data/BSS)
- 堆段(Heap)
- 栈段(Stack)
段页式
现代操作系统结合分页和分段:分段管理逻辑,分页管理物理。
- 段选择子 → 段描述符(含段基址)
- 段基址 + 段内偏移 → 线性地址(虚拟地址)
- 虚拟地址 → 分页机制 → 物理地址
x86 架构从 80386 开始使用段页式,但 Linux 通过"平坦模型"弱化分段(所有段基址为 0),实质上使用的是纯分页。
虚拟内存的实现
缺页中断(Page Fault)
访问未加载到物理内存的虚拟地址时,CPU 触发缺页中断:
- CPU 检查页表项,发现 Present 位为 0
- 操作系统缺页处理程序执行
- 从磁盘(Swap 或文件映射)读取页面到空闲页框
- 更新页表,设置 Present 位
- 重新执行触发缺页的指令
Swapping 与页面换出
当物理内存不足时,内核的页面回收机制执行:
- kswapd 内核线程:后台监测空闲内存水位,达到 low 水位时开始回收
- 直接回收:同步内存分配失败时触发
- OOM Killer:极端情况下终止进程释放内存
写时复制(Copy-on-Write, COW)
fork() 创建子进程时,不立即复制全部内存,而是:
- 父子进程共享同一物理页,页表标记为只读
- 任一进程写入时触发保护错误
- 内核分配新物理页,复制内容
- 更新页表为可写
优势:fork() 后立即 exec() 的场景完全避免复制,大幅提升效率。
Linux 内存管理关键命令
bash
# 查看内存使用
free -h
cat /proc/meminfo
# 查看进程内存映射
cat /proc/<PID>/maps
cat /proc/<PID>/smaps # 详细内存使用
# 查看页错误
ps -o minflt,majflt <PID>
# 大页配置
cat /proc/meminfo | grep Huge
# 内存压力测试
vmstat 1常见面试问题
- 虚拟内存解决了什么问题?没有虚拟内存会怎样?
- 多级页表为什么能节省内存?
- 缺页中断的处理流程?
- LRU 的硬件实现方式有哪些?
- Linux 的 OOM Killer 如何选择被终止的进程?
- Segment Fault 是怎么产生的?