操作系统内存管理详解:分页、分段、页面置换、Swap 与 OOM
内存管理的本质是把程序看到的虚拟地址和真实物理内存解耦:分页与页表负责映射和权限,TLB 负责给地址翻译提速,置换算法决定内存紧张时换谁出去,伙伴系统和 slab 则在内核里分管物理页的批发与零售。本文按”为什么需要、怎么映射、怎么置换、怎么分配”的顺序把这条链路讲透。
它解决什么问题
早期系统里程序直接操作物理地址,单程序还能凑合,多道程序一并发立刻暴露三个硬伤。
- 隔离失效:程序 A 往物理地址 1000 写数据,程序 B 恰好也把变量放在 1000,互相覆盖;更危险的是普通程序可能踩到内核自己的内存,系统稳定性无从谈起。
- 无法重定位:程序编译时不知道会被装进物理内存哪一段,地址一旦写死,换个装载位置就跑不了,更别说同时跑多个实例。
- 无法超售:物理内存就那么大,但程序总想要一大片连续空间,还希望”用到的才真的占内存”。
解法是引入虚拟地址空间:每个进程独占一套连续地址,落到哪些物理页由操作系统与 MMU 共同决定。隔离、重定位、超售三个目标,全部建立在这层映射之上。
核心原理:分段与分页怎么切地址空间
分段(Segmentation)按程序逻辑切:代码段、数据段、栈段各成一段,地址由段号加段内偏移构成,段表记录每段基址、界限与特权级。它贴近程序结构、便于按段设权限,但段长不固定,段与段之间会留下难以复用的空隙,即外部碎片;整理碎片又要做大粒度搬移,代价很高。
分页(Paging)换个思路:虚拟空间与物理内存都切成固定大小的页(Linux x86-64 通常 4 KB),虚拟地址拆成页号加页内偏移,页表记录虚拟页到物理页帧的映射。物理页可以离散分配,进程不再需要连续物理内存,外部碎片基本消失;代价是最后一页可能用不满(内部碎片),且页表自身要占空间。
折中方案是段页式:先按逻辑分段,段内再分页,兼顾逻辑保护与离散分配,但转换链条更长。现代通用操作系统以分页为绝对主力,x86-64 长模式下传统分段已退化为平坦模型,仅 FS/GS 保留实际用途(如线程本地存储)。
深入机制
多级页表与 TLB
单级页表的空间账算不过来:32 位地址、4 KB 页,一个进程就有 2^20 个页,每项 4 字节,页表独占 4 MB,且绝大部分条目对应从没用过的地址。多级页表的思路是只为用到的范围建下级表:顶级目录常驻覆盖全空间,下级页表按需创建。x86-64 主流采用四级分页,48 位虚拟地址按 9+9+9+9+12 位拆分,每级 512 项恰好占满一页。
分级省了空间,却让一次翻译最多要走四趟内存。TLB(Translation Lookaside Buffer,快表)是 MMU 内部的页表项缓存:命中直接得到物理页帧号,未命中再走页表并把结果回填。靠程序的访问局部性,TLB 命中率通常很高,查表成本被摊薄。2 MB 大页能让单个 TLB 项覆盖更大地址范围,这正是大页的主要收益来源。
虚拟地址 [虚拟页号 VPN | 页内偏移 offset] |
页面置换:内存不够时换谁出去
物理页耗尽又需要新页,必须挑一个”受害者”换出,挑得差就会刚换出又换入,白忙一场。
- OPT(最优置换):换出未来最久不会再用的页,缺页次数理论最少,但需要预知未来,只能当评判其他算法的基准。
- FIFO(先进先出):实现最简单,却不关心页面冷热,还会出现 Belady 异常——分配的页帧变多,缺页次数反而上升。
- LRU(最近最少使用):赌局部性,换出最久未访问的页,效果接近 OPT,但精确维护全局访问顺序的软硬件成本都太高。
- Clock(时钟/二次机会):LRU 的廉价近似。页面排成环形队列,每页配一个访问位;置换指针扫到访问位为 1 就清 0 放行(给第二次机会),扫到 0 就淘汰。一个位加一圈扫描,便低成本逼近 LRU。
真实 Linux 并不照搬某个教科书算法,而是用活跃/非活跃 LRU 链表、workingset、refault 检测、内存水位等机制组合近似,文件页与匿名页的回收路径也不同;说”Linux 用的就是 Clock”并不严谨。
物理页分配:伙伴系统与 slab
页表解决”怎么映射”,伙伴系统(Buddy System)解决”物理页从哪来”。空闲页按 2 的幂次大小分组:申请时找能满足请求的最小块,太大就一分为二;释放时若相邻伙伴也空闲就逐级合并。规则简单、能快速给出连续物理页、抑制外部碎片,但分配粒度是 2^order 个页,申请 65 KB 连续物理内存可能拿到 128 KB,存在内部浪费。
内核里还有大量比一页小得多的对象——inode、文件对象、网络描述符——按页分配太奢侈。slab(及后继 slub/slub 系)分配器架在伙伴系统之上:批量向伙伴系统拿页,再按对象大小切成缓存块反复复用,既省空间又省反复初始化的开销。两者是”页级批发”与”对象级零售”的分层关系。
工程实践
- JVM 堆只是地址空间的一部分:进程 RSS 还包含元空间、线程栈、JIT 代码缓存、DirectBuffer 等,RSS 超过
-Xmx不能直接断定堆泄漏。 - 大页按场景验证:数据库、JVM 用大页可缓解 TLB 压力,但 THP 的分配、清零与内存紧凑可能带来延迟毛刺,Redis 官方就建议关闭 THP。
- 内存指标要拆开看:Linux 会把空闲内存拿去做 Page Cache,
free很低不代表紧张;结合MemAvailable、Swap 活跃度、主缺页速率与 PSI 判断才有意义。 - Swap 是后备不是扩容:匿名页回收依赖 Swap 换出换入,工作集一旦长期超过物理内存,系统进入抖动,加 Swap 只是延缓失败。
对比与权衡
| 维度 | 分段 | 分页 | 段页式 |
|---|---|---|---|
| 划分依据 | 程序逻辑(代码/数据/栈) | 固定大小 | 先分段再分页 |
| 地址结构 | 段号 + 段内偏移 | 页号 + 页内偏移 | 段号 + 段内页号 + 偏移 |
| 碎片特征 | 外部碎片严重 | 内部碎片可控 | 内部碎片 |
| 共享与保护 | 按段授权,语义自然 | 按页授权,粒度细 | 两者兼顾 |
| 转换开销 | 查一次段表 | 多级页表 + TLB | 链条最长 |
面试视角
为什么需要虚拟地址空间?
直接暴露物理地址会带来互相覆盖、无法重定位、无法超售三个问题。虚拟地址让每个进程独占一套连续地址,落到哪些物理页由内核与 MMU 决定,隔离、保护与按需调页才有落脚点。
分页和分段的区别?
分段按逻辑模块切,长度不固定,保护共享直观但产生外部碎片;分页按固定大小切,物理页离散分配,外部碎片基本消除,代价是内部碎片与页表开销。现代系统以分页为主。
TLB 是什么,为什么离不开它?
TLB 是 MMU 里的页表项缓存。多级页表下每次翻译最多要多次访存,开销不可接受;TLB 利用局部性缓存热点映射,命中即直达物理页帧号,把查表成本摊薄到接近零。
LRU 效果好,为什么实际系统偏爱 Clock?
精确 LRU 要在每次访问时维护全局顺序,成本太高。Clock 只用一个访问位加环形扫描,给最近用过的页第二次机会,以极低成本逼近 LRU,是工程上的主流折中;真实内核则在此基础上叠加冷热链表与水位控制。
伙伴系统和 slab 各管什么?
伙伴系统管物理页批发:按 2 的幂分裂合并,快速提供连续页、抑制外部碎片。slab 管对象级零售:从伙伴系统拿页后切成固定大小的对象缓存复用,服务内核里大量小对象的高频分配释放。
小结
- 虚拟地址空间一次性解决隔离、重定位、超售三个问题,是内存管理的基石。
- 分页靠固定粒度换来离散分配,多级页表省下稀疏空间的开销,TLB 再把翻译成本压回去。
- 置换算法从 OPT 到 Clock 是效果与成本的权衡谱系,真实内核用组合机制近似而非单选。
- 伙伴系统按页批发、slab 按对象零售,二者分层协作,撑起内核的物理内存管理。

