内存管理是 Linux 内核最复杂且最核心的子系统之一。它不仅决定了系统资源的利用效率,更直接影响应用的响应延迟和系统稳定性。本文将从内核源码级别,全面剖析 Buddy System(伙伴系统)、Slab/Slob/Slub 分配器的工作原理,并结合实际案例讲解 OOM Killer 的行为机制与调优策略。
一、物理内存的组织架构:Node与Zone
Linux 内核采用 NUMA(Non-Uniform Memory Access)架构来管理物理内存。整个物理内存被组织为以下层次结构:
NUMA Node
└── Memory Zone
└── Page Frame (4KB)
每个 NUMA 节点(pg_data_t)包含多个内存区域(Zone),主要类型包括:
- ZONE_DMA:0-16MB,用于老旧 ISA 设备的 DMA 操作
- ZONE_DMA32:16MB-4GB,用于 32 位 DMA 设备
- ZONE_NORMAL:直接映射到内核虚拟地址空间(x86 下通常为 1GB 或 2GB)
- ZONE_HIGHMEM:超过直接映射范围的物理内存(仅 32 位系统)
可以通过以下命令查看当前系统的内存区域分布:
$ cat /proc/zoneinfo | head -100
Node 0, zone DMA
pages free 3960
min 12
low 15
high 18
spanned 4095
present 3997
managed 3975
protection: (0, 2006, 3007, 3684)
其中 watermark(min/low/high)是页面回收的关键阈值:当空闲页面低于 low 时,内核启动后台回收(kswapd);低于 min 时,直接进行同步回收(direct reclaim)。
二、Buddy System(伙伴系统)详解
2.1 核心思想
Buddy System 是物理页框(Page Frame)的分配器,负责以页(通常 4KB)为单位分配和释放连续物理内存。其核心机制是按需分裂与合并,通过维护 11 个空闲链表(order 0~10),分别管理 2<sup>n</sup> 个连续页面组成的内存块。
// 核心数据结构:free_area
struct free_area {
struct free_list free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
// 每个节点的内存区域中管理分页
struct zone {
struct free_area free_area[MAX_ORDER]; // MAX_ORDER 在部分版本为11
};
2.2 分配流程
alloc_pages() 是最常用的底层分配接口,其调用链为:
alloc_pages()
└── alloc_pages_node()
└── __alloc_pages_node()
└── __alloc_pages()
└── get_page_from_freelist() // 快速路径
└── __alloc_pages_slowpath() // 慢速路径(触发回收/压缩)
快速路径直接从未分配列表中摘取页面;慢速路径则触发直接内存回收(direct reclaim)、内存规整(compaction),甚至唤醒 OOM Killer。
2.3 伙伴关系的判断
两个页面块被称为"伙伴"需满足三个条件:大小相同(均为 2<sup>n</sup> 页)、物理地址连续、且合并后对齐到 2<sup>n+1</sup> 的边界。判断逻辑如下:
// 计算伙伴块的索引:buddy_pfn = pfn ^ (1 << order)
static inline unsigned long __find_buddy_pfn(unsigned long page_pfn, unsigned int order) {
return page_pfn ^ (1UL << order);
}
例如,order=2 时(4页=16KB),地址 0x10000~0x14000 的块的伙伴是 0x14000~0x18000,因其索引 X XOR 100 = X+4。
2.4 碎片问题与解决方案
Buddy System 长期运行后会产生外部碎片:虽然总空闲页足够,但没有足够大的连续物理块。内核通过页面迁移类型(page migration type)来缓解这一问题:
- MOVABLE:可移动页面(用户态内存),可通过页面迁移整理碎片
- RECLAIMABLE:可回收页面(文件缓存),可直接丢弃重建
- UNMOVABLE:不可移动页面(内核态 dma_alloc、slab 等)
从内核 5.10 年起,CONFIG_TRANSPARENT_HUGEPAGE 默认开启,通过 khugepaged 后台线程将小页合并为大页(2MB/1GB),有效减少 TLB miss。
三、Slab 分配器:小内存分配的艺术
3.1 为什么需要 Slab?
Buddy System 以页为单位分配对象,但内核频繁创建和销毁的对象(如 task_struct、inode、dentry)往往只有几十到几百字节。若直接调用 alloc_page(),内部碎片率接近 100%。Slab 分配器在 Buddy System 之上构建对象缓存池,实现高效的小对象分配。
3.2 Slab 的三种实现
Linux 支持三种 Slab 变体,当前主流为 SLUB:
| 实现 | 特点 | 适用场景 |
|---|---|---|
| Slab | 最早的实现,复杂的数据结构,维护开销大 | 已废弃 |
| Slob | 极简实现,用于嵌入式系统 | 内存小于64MB的系统 |
| SLUB | 为 SMP 优化,默认选项,per-CPU 缓存 | 默认选择 |
3.3 SLUB 的核心机制
// Slab 缓存描述符
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 快速缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // Node 级缓存
unsigned int size; // 对象实际大小(含对齐)
unsigned int object_size; // 用户请求大小
unsigned int offset; // 下一个空闲对象的偏移
struct kmem_cache_order_objects oo; // 每 slab 的 page order 与对象数量
};
分配优先级链:
kmem_cache_alloc()
└── ____cache_alloc()
└── cpu_slab // 第一步:per-CPU 快速缓存(无锁)
└── cpu partial // 第二步:per-CPU 部分空闲列表
└── node partial // 第三步:Node 级部分空闲列表
└── alloc_pages() // 第四步:向 Buddy 申请新页
3.4 kmem_cache 的实际使用
通过 /proc/slabinfo 可以查看所有 slab 缓存的状态:
$ sudo cat /proc/slabinfo | head -20
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ...
dentry 156823 158000 192 20 1 : tunables ...
inode_cache 24561 25000 648 5 2 : tunables ...
task_struct 256 264 5952 5 2 : tunables ...
signal_cache 380 400 1136 14 4 : tunables ...
各列含义:active_objs 为当前活跃对象数,num_objs 为总对象数(含已释放但缓存的),objsize 为单个对象字节数。
3.5 Slab 调试:检测内存泄漏与越界
CONFIG_DEBUG_KMEMLEAK 可追踪未被释放的分配:
# echo scan > /sys/kernel/debug/kmemleak
# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88800a3b2c00 (size 256):
comm "test", pid 1234, jiffies 4294901234
backtrace:
[<ffffffff81a2b3c4>] kmem_cache_alloc_trace+0x124/0x200
[<ffffffff81b5c6d7>] my_module_init+0x17/0x1000 [my_module]
四、OOM Killer:最后的防线
4.1 触发条件
当系统内存耗尽且直接回收都无法释放足够页面时,OOM Killer 被激活。触发点在 out_of_memory() 函数中:
alloc_pages()
└── __alloc_pages_may_oom()
└── out_of_memory()
└── select_bad_process() // 选择"最坏"进程
└── oom_kill_process() // 发送 SIGKILL
4.2 oom_score 的计算逻辑
每个进程的 OOM 评分存放在 /proc/[pid]/oom_score,计算方式为:
points = total_vm_pages * 1000 / total_ram_pages + (oom_score_adj * total_ram_pages / 1000 / 100)
其中:
- 基础分:进程占用内存占总内存比例 * 1000,得分越高越容易被杀
- 调整值:
oom_score_adj范围 -1000~1000,-1000 表示永不被 OOM杀死
关键影响因素包括:
- 长时间运行的进程得分更高(内存占用累积效应)
- root 进程得分降低 3%
- 直接硬件访问进程得分降低 3%
- 不能杀死的核心进程:init(pid 1)、内核线程、pid 2
4.3 如何保护关键进程
通过设置 oom_score_adj 保护关键服务:
# 保护数据库,永不被 OOM 杀死
$ echo -1000 > /proc/$(pidof mysqld)/oom_score_adj
# 或 systemd 方式(推荐):
# /etc/systemd/system/mysql.service.d/oom.conf
[Service]
OOMScoreAdjust=-1000
查看当前所有进程的 OOM 评分排名:
$ for pid in $(ls /proc | grep -E '^[0-9]+$'); do
if [ -f /proc/$pid/oom_score ]; then
echo "$(cat /proc/$pid/oom_score 2>/dev/null) $(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ')"
fi
done | sort -rn | head -20
4.4 OOM 行为调优参数
内核 vm.overcommit_memory 控制系统是否允许超额内存分配:
| 值 | 行为 | 适用场景 |
|---|---|---|
| 0(默认) | 启发式 overcommit,允许适量超分 | 通用场景 |
| 1 | 总是允许 overcommit,永不拒绝 | 科学计算(如 MATLAB) |
| 2 | 严格模式:CommitLimit = Swap + RAM * overcommit_ratio% | 数据库、金融系统 |
# 查看当前 CommitLimit
$ grep -i commit /proc/meminfo
CommitLimit: 16565676 kB
Committed_AS: 12367890 kB
五、实战调优案例
5.1 容器内存限制导致频繁 Direct Reclaim
某 Java 虚拟机容器限制为 4GB 内存,访问量大时频繁触发直接回收,P99 延迟飙升。
排查步骤:
# 1. 检查直接回收次数
$ vmstat 1 10
procs -----------memory---------- ---swap-in-----------
r b swpd free buff cache si so bi bo in cs
1 4 0 452000 203400 1998100 0 0 0 380 2140 3985
# si/so 不为 0 说明系统压力大
# b 列(不可中断睡眠进程数)> 0 说明存在 IO/内存阻塞
# 2. 检查回收统计
$ sar -B 1 10
pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s
128.00 1024.00 1200 4.3 2800 1500 8000
# pgscand/s 持续 > 0 表示前台直接回收频繁
# 3. 检查 watermark
$ cat /proc/zoneinfo | grep -E "Node|zone|high|min|low"
# 解决:增大容器内存限制 + 调整 watermark_scale_factor
$ echo 125 > /proc/sys/vm/watermark_scale_factor # 默认 10,增大提高 watermark
5.2 OOM 导致数据库被杀的应急排查
凌晨收到报警 MySQL 进程被 OOM Killer 杀死。分析步骤:
# 1. 查内核日志
$ dmesg -T | grep -i "oom\|Out of memory\|Killed process"
[Tue Jan 14 03:15:22 2025] Out of memory: Killed process 1234 (mysqld)
[Tue Jan 14 03:15:22 2025] oom_memcg: memcg=/system.slice/mysql.service ...
# 2. 查看 OOM Killer 详细评分
$ dmesg -T | grep -B20 "Out of memory"
[ +0.000000] oom-kill: constraint=CONSTRAINT_MEMCG,...
# 3. 解决:设置 cgroup memory.high 做软限制 + 保护进程
$ mkdir /sys/fs/cgroup/mysql.slice
$ echo 2147483648 > /sys/fs/cgroup/mysql.slice/memory.high # 2GB 软限制
$ echo 2684354560 > /sys/fs/cgroup/mysql.slice/memory.max # 2.5GB 硬限制
$ echo -1000 > /proc/$pid/oom_score_adj # 不被杀死
5.3 Slab 内存泄漏导致系统卡顿
排查 dentry 缓存无法释放的问题:
# 直接释放 slab 缓存(临时缓解)
$ echo 2 > /proc/sys/vm/drop_caches # 释放 dentry 和 inode
$ echo 3 > /proc/sys/vm/drop_caches # 释放所有缓存(dentry+inode+page cache)
# 永久解决方案:调整 vfs_cache_pressure
$ echo 50 > /proc/sys/vm/vfs_cache_pressure # 默认 100,降低值减少缓存回收压力
$ echo 10000 > /proc/sys/vm/min_free_kbytes # 设置最小空闲内存(根据实际内存调整)
六、内核演进趋势
Linux 内存管理子系统仍在快速演进中,未来的主要方向包括:
- Folio(大叶页):从 5.16 引入,统一管理大页和小页,减少内核代码复杂度
- MGLRU(Multi-Gen LRU):5.18 引入,通过多代 LRU 更精准地识别冷页面,减少误回收
- Memory Bypass Architecture:配合 CXL、PMem 等新内存技术,构建异构内存管理
- Per-Process THP 控制:细粒度控制每个进程是否启用透明大页
七、总结与排查速查表
| 问题现象 | 排查入口 | 常见原因 |
|---|---|---|
| 系统抖动、延迟抖动 | sar -B + vmstat | Direct Reclaim 频繁,内存压力过大 |
| 进程突然被杀 | dmesg | grep oom | OOM Killer 触发 |
| free 显示内存足够但分配失败 | /proc/zoneinfo | 外部碎片 + 不可迁移页面过多 |
| Slab 内存持续增长 | /proc/slabinfo + kmemleak | 模块内存泄漏或缓存膨胀 |
理解内存管理的底层机制,是成为Linux系统高手的关键一步。Buddy System 保证了物理内存的高效分配,Slab 分配器优化了高频小对象的复用,OOM Killer 系统运行的最后屏障。三者共同构成了 Linux 可靠的内存管理基石。

发表评论 取消回复