Linux 内存管理最让人头疼的地方,不是某个概念难,而是概念太多、太碎。页表、VMA、缺页、伙伴系统、swap、OOM 每个都知道一点,但很容易串不起来。
我更建议先抓住这条顺序,再回头看细节:
虚拟地址 -> 页表映射 -> 缺页建立映射 -> 物理页分配 -> 内存紧张时回收 / 换出 -> 再不够就 OOM
把这条线理顺之后,再看 mm_struct、伙伴系统、page cache、swap,就不容易散。
1. 为什么操作系统必须做内存管理
如果程序直接操作物理地址,会马上遇到几个问题:
- 不同程序之间互相踩内存
- 一个程序崩溃可能直接带崩整个系统
- 程序装载位置必须固定,可移植性差
- 空闲内存难以灵活复用
因此现代操作系统会给每个进程提供一个“看起来连续且独立”的虚拟地址空间。应用程序以为自己在独占一整块内存,实际上底层由内核和硬件负责把这些虚拟地址翻译成真正的物理页。
这就是内存管理最先解决的问题:隔离和抽象。
2. 从分段到分页,为什么最后都落到页
分段解决了什么
分段更强调逻辑划分,把程序分成代码段、数据段、栈段等区域。它的好处是更符合程序结构,也更容易表达权限。
但分段的问题在于:
- 粒度偏大
- 不利于细粒度调度和换入换出
- 容易产生外部碎片
分页为什么更适合现代系统
分页把地址空间切成固定大小的页,例如 4 KB。页大小固定之后,内核更容易:
- 分配和回收内存
- 做按需加载
- 做缺页处理
- 做页面换入换出
Linux 后面那一大串机制,几乎都是围着“页”在转。看页表、缺页中断、伙伴系统时,脑子里最好一直记着:内核不是按“变量”分配内存,它按页干活。
3. 虚拟地址、物理地址、MMU 到底怎么串起来
进程执行一条读写指令时,用的是虚拟地址,不是物理地址。地址转换的参与者有三层:
- 软件维护页表
- MMU 负责查表和地址翻译
- TLB 缓存热点翻译结果
把这件事粗略展开,大概就是:
- CPU 发起虚拟地址访问
- 先查 TLB
- 命中则快速得到物理地址
- 未命中则根据页表重新翻译
- 如果页表里压根没有有效映射,就触发缺页异常
所以性能上常听到的 TLB miss,不是“找不到内存”,而是“地址翻译缓存没命中,需要重新走页表流程”。
4. 进程看到的地址空间是什么样的
Linux 进程并不是简单拿到一大块连续地址,而是由多个区域拼出来。内核用 mm_struct 描述进程整体内存上下文,用 vm_area_struct 描述一段段 VMA。
常见区域包括:
- 代码段
- 数据段
- BSS
- 堆
- 栈
- 动态库映射区
mmap匿名映射或文件映射区
VMA 很重要,因为很多内存行为都依赖它提供上下文信息:
- 这块地址是匿名页还是文件映射
- 可读、可写、可执行权限是什么
- 触发缺页时应该如何补页
换句话说,页表决定“地址怎么映射”,VMA 决定“这段地址为什么存在、该按什么规则处理”。
5. 缺页中断是整个体系的关键枢纽
缺页中断不是异常中的“出错”,它往往是正常机制的一部分。
进程申请一块内存时,虚拟地址空间可以先建立好,但物理页不一定立刻分配。只有真正访问到该地址时,内核才会发现:
- 当前还没有物理页
- 或者页已经被换出
- 或者是文件映射页尚未载入
此时内核会根据 VMA 和页表状态决定接下来做什么:
- 分配新的匿名页
- 把文件内容读入 page cache
- 从 swap 把匿名页换回内存
- 如果访问非法地址,则发送段错误
所以“缺页”并不等于出错。更关键的是分清:
- 合法的按需缺页
- 非法访问导致的异常缺页
6. 物理内存是怎么被管理的
虚拟内存是进程视角,物理页管理是内核视角。Linux 内核需要解决两个不同层级的问题:
- 大块页框怎么分配
- 小对象怎么高效复用
伙伴系统
伙伴系统负责页级分配。它的思路是按 2 的幂次管理内存块:
- 需要大块时尽量直接拿
- 没有合适块时向上拆分
- 释放时如果“伙伴块”也空闲,就向上合并
这种方式优点是简单、适合页框管理,缺点是会有一定内部碎片。
slab / slub
内核里大量分配并不是整页,而是各种小对象,例如 inode、dentry、task 相关结构。直接拿页来切太浪费,于是需要 slab 类分配器来做对象缓存。
它们解决的问题是:
- 小对象高频分配释放
- 减少重复初始化成本
- 降低碎片
所以可以简单记成:
- 伙伴系统负责“页”
- slab / slub 负责“页上的小对象”
7. 页面回收为什么会牵扯 page cache 和匿名页
当物理内存紧张时,内核不会立刻 OOM,而是优先尝试回收。回收对象主要有两类:
page cache
文件数据读入内存之后会形成 page cache。它的优势是:
- 已经写回磁盘的干净页可以直接丢弃
- 再次访问时可以重新从文件系统加载
因此干净的 page cache 一般回收成本较低。
匿名页
匿名页通常来自堆、栈、匿名 mmap。这类页没有现成文件作为后备存储,如果要回收,往往需要换出到 swap。
因此匿名页的回收成本通常更高,因为它会涉及:
- 写 swap
- 后续访问时再从 swap 换入
这也是为什么很多系统一到内存吃紧就会明显卡顿,因为它已经开始频繁换页了。
8. swap、内存规整、huge page、OOM 各自解决什么问题
swap
swap 的作用不是“让系统变快”,而是提供更大的腾挪空间,让匿名页在物理内存不足时有地方暂存。它解决的是“内存不够时能不能继续活下去”,不是“性能优化”。
内存规整
长时间运行后,空闲页可能很多,但连续大块页不足。这时内存规整会尝试迁移页面,把碎片整理成更大的连续区域,方便高阶分配或 huge page。
huge page
大页减少了页表项数量,也减少了 TLB miss 频率,对数据库、虚拟化、内存密集型场景更有价值。但它要求更大的连续物理块,管理也更复杂。
OOM
当下面这些手段都不够时:
- 普通回收
- swap
- 内存规整
内核就可能触发 OOM killer,杀掉部分进程释放内存。OOM 不是第一反应,而是最后兜底。
9. 理解内存问题时应该怎么观察
如果只是背概念,遇到线上问题还是容易慌。更实用的思路是按现象反推:
现象 1:程序一访问某块内存就崩
优先怀疑:
- 非法地址访问
- VMA 权限不对
- 页表映射不存在
现象 2:系统越来越卡,但 CPU 不一定高
优先怀疑:
- page reclaim 频繁
- swap in / swap out 频繁
- page cache 和匿名页争抢内存
现象 3:明明还有剩余内存,却申请大块内存失败
优先怀疑:
- 外部碎片
- 高阶页分配失败
- 需要规整或 huge page 相关配置不合适
现象 4:系统突然杀进程
优先从 OOM 日志看:
- 哪个进程被杀
- 当时 anon/rss/page cache 各占多少
- 是否存在明显泄漏或异常缓存膨胀
10. 推荐的学习和理解顺序
如果要把 Linux 内存管理学得更稳,建议按这条顺序推进:
- 虚拟地址和物理地址区别
- 分页、页表、TLB
mm_struct和 VMA- 缺页中断
- 伙伴系统和 slab / slub
- page cache、匿名页、swap
- reclaim、compaction、OOM
不要一开始就钻内核细节,否则很容易被数据结构淹没。
11. 总结
Linux 内存管理并不是单一模块,而是一整套协作链路:
- 硬件提供地址翻译能力
- 内核维护页表和 VMA
- 分配器管理页和对象
- 回收机制在紧张时腾挪内存
- 最后由 OOM 兜底
真把这套东西摸顺之后,价值不在于你会背多少结构体名,而是碰到内存问题时,能大概判断它更像是哪一类:
- 地址映射问题
- 页分配问题
- 回收压力问题
- swap 问题
- 还是 OOM 问题