分页 — 靠翻译地址换来的自由
一句话总结
把虚拟地址按固定大小的页映射到物理页框,可以消除外部碎片,并为每个进程提供独立地址空间。
为什么需要它
早期系统给每个进程一整块连续内存,很快就遇到外部碎片:空闲空间总量足够,却没有足够大的连续区块,无法载入新进程。内存紧缩(compaction)虽然能把空闲区聚拢,但期间系统必须停顿。
分页的洞见很简单:取消连续存放的要求。 把地址空间切成固定大小(通常 4KB)的页,物理内存也切成同样大小的页框。每一页可放入任意页框,只需用表记录其位置。
工作原理
虚拟地址分为两部分:高位是页号,低位是偏移量。页大小为 4KB 时,偏移量占 12 位。地址转换用页号查询页表并换成页框号,偏移量原样拼接,因此页内相对位置不变。
这会立即产生性能问题。页表位于内存中,每次访问数据都要先访问一次页表,相当于读取内存两次。解决它的缓存是 TLB(Translation Lookaside Buffer)。TLB 保存近期转换结果,命中时无需额外访问内存即可得到物理地址。
有效访问时间可这样计算:TLB 访问 20ns、内存访问 100ns、TLB 命中率 80% 时:
- 命中:20 + 100 = 120ns
- 未命中:20 + 100(页表)+ 100(实际数据)= 220ns
- 有效访问时间 = 0.8×120 + 0.2×220 = 140ns
命中率提高到 98% 后,结果为 122ns。这说明几个百分点的命中率就能左右整体性能。
64 位地址空间中页表本身会非常庞大,因此使用多级页表。地址被分成多段,页表像树一样分层,只创建实际使用的分支。x86-64 通常有 4 级。级数越多,TLB 未命中成本越高,所以大内存工作负载常使用更大页尺寸的大页(huge page)。2MB 页覆盖同样内存所需的 TLB 条目只有原来的 1/512。
实际工作中的表现
数据库和 JVM 等随机遍历大堆的程序经常讨论大页,原因就在 TLB。但 Linux 透明大页(THP)的内存整理可能给数据库带来尾延迟,因此 PostgreSQL、Redis 文档有时不建议使用 always,而是建议使用 madvise 或禁用。再好的功能也要根据工作负载的访问模式判断。
后续测验将确认什么
确认你能拆分地址,并计算 TLB 命中率如何改变有效访问时间。