引言
Linux 内核的内存管理是操作系统最核心的子系统之一。它不仅负责物理内存的分配与回收,还通过虚拟内存机制为每个进程提供了独立且巨大的地址空间。对于系统管理员和底层开发者而言,深入理解内核内存管理的原理与机制,是排查性能瓶颈、优化系统表现的关键。
本文将从物理页面管理出发,深入剖析 Buddy 分配系统、Slab 分配器、内存碎片化等核心概念,并结合实际案例展示内存调优的实战方法。
一、物理页面管理:一切的基础
物理页面(Page)是内核内存管理的最小单位,通常为 4KB。内核使用 struct page 结构体来跟踪系统中的每一个物理页面。
1.1 页帧与 PFN
每个物理页面都有一个唯一的编号,称为页帧号(Page Frame Number, PFN)。PFN 与物理地址的关系为:
PFN = 物理地址 / PAGE_SIZE
内核通过 pfn_to_page() 和 page_to_pfn() 两个宏实现 PFN 与 struct page 之间的相互转换。
1.2 内存区域(Zone)
由于硬件架构的限制,内核将物理内存划分为不同的区域(Zone):
| 区域 | 说明 | 典型范围 |
|---|---|---|
| ZONE_DMA | DMA 可访问的低端内存 | 0-16MB |
| ZONE_NORMAL | 内核可直接线性映射的内存 | 16MB-896MB |
| ZONE_HIGHMEM | 动态映射的高端内存 | 896MB以上(32位) |
在 64 位系统中,地址空间极其庞大,ZONE_HIGHMEM 不再需要,所有物理内存都直接映射在 ZONE_NORMAL 中。
二、Buddy 系统:伙伴分配算法
Buddy 系统是内核物理页面分配的核心算法,它负责以页为单位进行内存的分配与回收。
2.1 算法原理
Buddy 系统将空闲页面组织为 11 个链表,分别管理 1、2、4、8、...、1024 个连续页面(即阶为 0 到 10)。当请求 N 个页面时:
- 找到能满足需求的最小阶(order),使得 2order >= N
- 若该阶有空闲块,直接分配
- 若没有,向上递归查找更大的空闲块,将其分裂为两半(Buddy),一半分配,另一半放入低阶链表
- 释放时,检查相邻的 Buddy 块是否空闲,若空闲则合并为更大的块
2.2 避免碎片化的努力
Buddy 系统天然倾向于产生外部碎片——即由于分配和释放的随机性,导致空闲页面散落在不连续的区域中。为此,Linux 引入了可移动页面分组机制:
- 不可移动:内核数据结构(如页表、task_struct)
- 可回收:页缓存、文件映射页面
- 可移动:用户空间页面、可重定位的内核分配
在页面回收时,将相似类型的页面聚集在一起,减少碎片化程度。
三、Slab 分配器:小对象的高效管理
Buddy 系统以页为最小单位,但内核经常需要分配远小于 4KB 的对象(如 task_struct、inode、dentry 等)。若直接通过 Buddy 分配整页,将造成严重的内部碎片。
3.1 Slab 设计思想
Slab 分配器在 Buddy 系统之上构建了一层缓存(Cache),每个缓存专门管理某一类大小的对象:
- Slab:从 Buddy 分配器获取一个或多个页面,划分为固定大小的对象槽
- Cache:管理多个 slab,每个 cache 对应一种对象类型
- Constructor / Destructor:对象构造和析构函数,实现初始化复用
3.2 Slab 的状态
每个 slab 处于三种状态之一:
- Full:所有对象槽都被分配
- Partial:部分空闲,优先从此分配新对象
- Empty:全部空闲,内存紧张时返还给 Buddy 系统
内核优先从 Partial slab 分配对象,若不存在则从 Buddy 获取新页面创建 slab。
3.3 SLAB / SLUB / SLOB 三种实现
| 实现 | 特点 | 适用场景 |
|---|---|---|
| SLAB | 经典实现,丰富的调试选项 | 早期内核 |
| SLUB | 当前默认实现,简化元数据、更好的多核扩展性 | 通用场景 |
| SLOB | 极简实现,内存开销极小 | 嵌入式系统 |
四、虚拟内存与页表
每个进程拥有独立的虚拟地址空间,内核通过将虚拟地址映射到物理地址实现内存隔离。
4.1 多级页表
x86_64 架构使用四级页表:
PML4 → PDPT → PD → PT → Page
每级页表占用 9 位地址(512 项),最终指向 4KB 页面。这种多级结构使得未使用的地址区域无需分配中间页表,极大节省内存。
4.2 大页(Huge Pages)
传统页表只能映射 4KB 页面,对于需要大型连续内存的数据库应用,页表项数量巨大,TLB(Translation Lookaside Buffer)命中率低。大页机制允许使用 2MB 或 1GB 的大页面:
- 2MB 大页:跳过 PT 层,PD 项直接指向 2MB 物理页面
- 1GB 大页:跳过 PD 层,PDPT 项直接指向 1GB 物理页面
使用大页可显著提升 TLB 命中率,数据库场景下性能提升可达 30%。
五、页面回收与内存压力管理
当系统内存不足时,内核需要回收页面以释放内存。
5.1 LRU 链表
Linux 使用改进的 LRU(Least Recently Used)算法,将页面分为:
- Active 链表:最近被访问过的页面
- Inactive 链表:较长时间未被访问的页面
页面在两个链表之间移动:长时间未被访问的 Active 页面降级为 Inactive;Inactive 页面被重新访问则升级为 Active。回收时优先从 Inactive 链表尾部拿取页面。
5.2 kswapd 守护进程
kswapd 是内核的页面回收守护进程,它在后台周期性检查内存水位:
- High:内存充足,kswapd 休眠
- Low:内存开始紧张,kswapd 开始异步回收
- Min:内存极度紧张,触发直接回收(direct reclaim),分配进程自身阻塞并执行回收
直接回收会严重影响应用性能,应尽量避免。
5.3 OOM Killer
当直接回收仍无法获得足够内存时,内核触发 OOM Killer,根据算法计算每个进程的 "badness score",杀死得分最高的进程以缓解内存压力。
可以通过 /proc/<pid>/oom_score_adj 调整进程的 OOM 优先级,保护关键服务。
六、内存调优实战案例
6.1 数据库场景:减少直接回收
某 MySQL 服务器在高并发写入时出现频繁抖动,vmstat 1 显示 si/so(swap in/out)数值极高,sar -B 1 显示 pgscank(kswapd 扫描速率)飙升。
解决方案:
# 降低 swappiness,减少页缓存回收倾向
echo 10 > /proc/sys/vm/swappiness
# 提高 min_free_kbytes,提前唤醒 kswapd
echo 262144 > /proc/sys/vm/min_free_kbytes # 256MB
# 设置 MySQL 的 OOM 保护
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj
6.2 透明大页(THP)的双面性
透明大页(Transparent Huge Pages)尝试自动将小页面合并为大页,但某些场景下会导致延迟尖刺:
# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 对于延迟敏感的实时应用,禁用 THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
数据库(如 Redis、MongoDB)通常建议禁用 THP,避免内存合并操作引起的延迟抖动。
6.3 内存碎片化排查
使用 grep -i "fragment" /proc/buddyinfo 检查内存碎片化程度。若高阶连续页面(order >= 3)接近 0,说明碎片化严重。
缓解措施:
# 手动触发内存整理
echo 1 > /proc/sys/vm/compact_memory
# 调整 compaction 激进程度
echo 100 > /proc/sys/vm/extfrag_threshold
七、关键参数速查表
| 参数 | 默认值 | 说明 |
|---|---|---|
| vm.swappiness | 60 | Swap 倾向,0=尽量不swap,100=积极swap |
| vm.dirty_ratio | 20 | 脏页占总内存的最大百分比 |
| vm.dirty_background_ratio | 10 | 后台回写启动的百分比阈值 |
| vm.min_free_kbytes | 自动计算 | 最低保留空闲内存(KB) |
| vm.overcommit_memory | 0 | 0=启发式,1=总是允许,2=限制commit |
| vm.zone_reclaim_mode | 0 | 0=不从其他zone回收,1=优先本地zone回收 |
八、总结
Linux 内核内存管理是一个庞大而精密的系统,从底层的 Buddy 页面分配,到 Slab 小对象管理,再到虚拟内存映射与页面回收,每一层都经过精心设计与持续优化。理解这些机制,不仅能帮助我们在系统出现内存问题时精准定位,更能指导我们在应用层做出更合理的内存使用策略。
在实际调优中,建议遵循"观察-分析-调整-验证"的循环,善用 vmstat、sar、slabtop、numastat 等工具,结合应用场景制定针对性的内存策略。

发表评论 取消回复