💻CS 操作系统

虚拟内存与页表

难度:⭐⭐ | 高频指数:🔥🔥🔥

面试回答

常见问法

  • 为什么需要虚拟内存?
  • 页表是什么?多级页表解决什么问题?
  • TLB 是什么?为什么需要它?
  • 缺页中断的流程是什么?
  • malloc 底层是怎么实现的?
  • 什么是内存映射文件?

回答

虚拟内存是操作系统提供的一层抽象,让每个进程认为自己拥有独立的、连续的地址空间。实际的物理内存由操作系统通过页表进行映射和管理。

为什么需要虚拟内存:

  1. 隔离性:每个进程有独立地址空间,互不干扰
  2. 简化编程:程序员不需要关心物理内存布局
  3. 内存超卖:所有进程的虚拟内存总和可以超过物理内存(靠换页)
  4. 共享:多个进程可以映射同一物理页(如共享库)
  5. 保护:通过页表权限位实现读/写/执行权限控制

页表是虚拟地址到物理地址的映射表。虚拟地址分为”页号 + 页内偏移”,通过页号查页表得到物理页框号,拼上偏移就是物理地址。

多级页表解决的是页表本身太大的问题。以 32 位系统为例,4KB 页大小需要 100 万个页表项(4MB),如果用二级页表,只需要为实际使用的虚拟地址分配页表页,大幅节省内存。

追问

1. TLB 是什么?

TLB(Translation Lookaside Buffer)是页表的高速缓存,位于 CPU 内部。因为每次内存访问都要查页表(多级页表要查多次),TLB 缓存最近使用的页表项,命中时一次就能完成地址转换。TLB 未命中才需要走完整的页表查询(page table walk)。

2. 缺页中断流程?

  1. CPU 访问虚拟地址,查页表发现该页不在物理内存(有效位为 0)
  2. 触发缺页中断(Page Fault),陷入内核
  3. 内核检查该地址是否合法(是否在进程的虚拟地址空间内)
  4. 如果合法,在物理内存中找一个空闲页框
  5. 如果没有空闲页框,用页面置换算法(LRU 等)换出一页
  6. 从磁盘(swap 或文件)读入所需页面
  7. 更新页表项,设置有效位
  8. 重新执行触发缺页的指令

3. malloc 底层实现?

  • 小块内存(< 128KB):调用 brk/sbrk 扩展堆顶
  • 大块内存(>= 128KB):调用 mmap 分配独立的虚拟内存区域
  • malloc 内部维护空闲链表,尽量复用已释放的内存块
  • glibc 的 ptmalloc 使用 arena + bin 的分配策略

原理展开

1. 虚拟地址到物理地址的转换

以 32 位系统、4KB 页为例:

虚拟地址(32 位):
┌──────────────┬──────────────┐
│  页号(20位) │ 页内偏移(12位)│
└──────────────┴──────────────┘

转换过程:
虚拟页号 → 查页表 → 物理页框号
物理地址 = 物理页框号 × 页大小 + 页内偏移

2. 多级页表

问题:32 位地址空间,4KB 页,需要 2^20 = 100 万个页表项。每项 4 字节,页表就要 4MB。每个进程都要 4MB 页表,太浪费。

二级页表方案

虚拟地址(32 位):
┌────────────┬────────────┬──────────────┐
│ 一级页号(10)│ 二级页号(10)│ 页内偏移(12)  │
└────────────┴────────────┴──────────────┘

一级页表(页目录):1024 项,每项指向一个二级页表
二级页表:1024 项,每项指向一个物理页框

好处:只有实际使用的虚拟地址范围才需要分配二级页表。大部分进程只用了很小一部分地址空间,节省大量内存。

64 位系统(x86-64)使用四级页表:PGD → PUD → PMD → PTE,每级 9 位索引。

3. TLB 工作原理

CPU 访问虚拟地址


查 TLB(硬件并行比较)

    ├── 命中 → 直接得到物理地址(1 个时钟周期)

    └── 未命中 → Page Table Walk(多次内存访问)


              更新 TLB,返回物理地址

TLB 的关键特性:

  • 容量小(通常 64-1024 项),但命中率高(> 99%)
  • 全相联或组相联结构
  • 进程切换时需要刷新 TLB(或使用 ASID 标记)
  • TLB miss 的代价:多级页表需要多次内存访问

4. 页面置换算法

算法原理优缺点
FIFO淘汰最早进入的页简单但可能淘汰常用页
LRU淘汰最久未使用的页效果好但实现开销大
ClockLRU 的近似,用访问位实际系统常用
LFU淘汰使用频率最低的页对突发访问不友好

Linux 使用的是改进的 Clock 算法(双链表 + 活跃/不活跃列表)。

5. malloc 的底层机制

// 小内存:brk 扩展堆
void* p = malloc(64);  // 内部调用 brk/sbrk

// 大内存:mmap 映射
void* p = malloc(256 * 1024);  // 内部调用 mmap

brk vs mmap:

对比项brkmmap
分配方式移动堆顶指针映射新的虚拟内存区域
释放只有堆顶连续空闲才能归还可以直接 munmap 归还
碎片容易产生外部碎片不会(独立区域)
适用小块频繁分配大块内存

ptmalloc 的 bin 结构:

  • fast bins:小块(< 64B),单链表,不合并
  • small bins:中等块,双链表,精确大小匹配
  • large bins:大块,按大小范围排序
  • unsorted bin:刚释放的块暂存,下次分配时整理

6. 内存映射文件(mmap)

// 将文件映射到进程地址空间
int fd = open("data.bin", O_RDWR);
void* addr = mmap(NULL, file_size, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);

// 直接通过指针读写文件内容
char* data = (char*)addr;
data[0] = 'A';  // 修改会写回文件

munmap(addr, file_size);

mmap 的优势:

  • 避免 read/write 的用户态-内核态数据拷贝
  • 利用页缓存,多个进程可共享同一映射
  • 适合大文件随机访问
  • 共享库(.so)就是通过 mmap 加载的

mmap 的注意事项:

  • 不适合小文件频繁读写(页对齐浪费)
  • 写入不是立即落盘(需要 msync 或等内核回写)
  • 映射区域访问越界会触发 SIGBUS

7. 虚拟内存的其他应用

  • Copy-on-Write:fork 后共享页面,写时才复制
  • Demand Paging:页面首次访问时才分配物理内存
  • Memory-mapped IO:将设备寄存器映射到虚拟地址空间
  • Guard Page:栈溢出检测,栈底放一个不可访问的页

易错点

  • 说”虚拟内存就是用磁盘当内存”——这只是虚拟内存的一个功能(swap),不是全部。核心是地址空间隔离和抽象。
  • 混淆”页”和”段”——现代系统主要用分页,分段已经很少用了。
  • 认为 malloc 直接调用系统调用——malloc 内部有内存池,大部分分配不需要系统调用。
  • 说”TLB 是页表的一部分”——TLB 是独立的硬件缓存,不是页表的组成部分。
  • 忘记多级页表的好处——不是为了加速查找,而是为了节省页表本身占用的内存。
  • 认为 mmap 分配的内存立即占用物理内存——实际是 demand paging,首次访问才分配。

记忆技巧

  • 虚拟内存三大好处:隔离、超卖、共享
  • 地址转换链路:虚拟地址 → TLB/页表 → 物理地址
  • 多级页表口诀:不是为了快,是为了省(省页表内存)
  • 缺页中断流程:查页表 → 不在 → 中断 → 找空闲帧 → 读磁盘 → 更新页表 → 重执行
  • malloc 分界线:128KB 以下 brk,以上 mmap
  • TLB 一句话:页表的 cache,命中率 99%+

面试速答版

虚拟内存让每个进程拥有独立的地址空间,核心好处是隔离、超卖和共享。地址转换通过页表完成:虚拟页号查页表得到物理页框号。多级页表解决的是页表本身太大的问题,只为实际使用的地址范围分配页表页。TLB 是页表的硬件缓存,命中率通常 99% 以上。缺页中断发生在访问的页不在物理内存时,内核会从磁盘读入并更新页表。malloc 小块用 brk 扩展堆,大块用 mmap 映射独立区域,内部维护空闲链表减少系统调用。

Related · 操作系统