Linux 内核内存管理深度实战:页分配器、SLUB 分配器与 OOM Killer 全链路剖析
内存管理是 Linux 内核最核心的子系统之一,它直接影响着系统的吞吐量、延迟和稳定性。从底层的 buddy system 页分配器,到上层的 SLUB slab 分配器,再到紧急情况下的 OOM Killer,整个内存管理栈涉及众多精巧的设计和复杂的生产级调优参数。本文将从内核源码、性能调优和故障排查三个维度,对 Linux 内存管理进行全方位的深度实战解析。
一、Buddy System 页分配器:物理内存的基石
1.1 核心思想与 free_area 结构
Linux 内核采用 Buddy(伙伴)系统来管理物理内存页。其核心思想是将内存划分为不同阶(order)的连续页面块,每个阶包含 2^order 个连续物理页。当需要分配内存时,从对应阶的空闲链表中取出一个块;如果该阶没有空闲块,则从更高的阶"分裂"出一个块,一半用于分配,另一半降阶放入低阶链表。
struct free_area 是 buddy system 的核心数据结构,在 include/linux/mmzone.h 中定义:
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
每个内存区域(zone)内部按照 migrate type 进一步划分,这是 Linux 为解决碎片化问题引入的页面迁移机制。不同的 migrate type 将页面分为可移动、可回收、不可移动等类别,使得在内存碎片整理(compaction)时能够高效地迁移页面。
1.2 分配路径:从 fast path 到 slow path
页面分配遵循"先快后慢"的两级策略:
- Fast path(快速路径):使用 per-CPU 的页面缓存(page allocator fastpath),从 pcp->count 充足时直接返回单个页面,无需加锁或访问 zone->lock。这一步可以分配 order-0 的页面。
- Slow path(慢速路径):当 fast path 失败时,进入
__alloc_pages_slowpath(),可能触发直接回收(direct reclaim)、内存规整(compaction),甚至调用 OOM Killer。
对于高阶分配(order > 0),例如需要连续 4 个页面的 scatter-gather DMA,slow path 还需处理失败的回退策略:尝试降低分配阶、启用 reclaim、触发 compaction。
1.3 页面迁移类型与碎片避免
Linux 定义了以下核心迁移类型:
enum migratetype {
MIGRATE_UNMOVABLE, // 内核数据结构(不可移动)
MIGRATE_MOVABLE, // 用户页面(可移动)
MIGRATE_RECLAIMABLE, // 可回收(文件缓存等)
MIGRATE_PCPTYPES, // per-cpu pageset 专用
MIGRATE_HIGHATOMIC, // 原子分配保留池
MIGRATE_ISOLATE, // 隔离用(不可分配)
MIGRATE_TYPES
};
这种分类使得在 __alloc_pages_direct_compact() 阶段,可以高效地将可移动页面迁移到一起,腾出连续的物理空间。
二、SLUB 分配器:内核对象的高效分配
2.1 SLAB vs SLUB vs SLOB:演进之路
SLAB 分配器自 Linux 2.3 引入以来经历了三代演进:
- SLAB:经典实现,每个 slab 包含多个对象,使用复杂的着色(coloring)机制减少缓存冲突。但管理结构开销大,适用于早期系统。
- SLUB(Unqueued SLAB):自 2.6.23 成为默认分配器。去掉了 per-CPU 队列的复杂锁机制,每个 slab 只有一个 freelist,大大简化了实现并提升了 NUMA 扩展性。
- SLOB:极简实现,适用于嵌入式系统(如早期的 Gumstix),使用简单的 first-fit 算法,适合内存小于 16MB 的设备。
2.2 SLUB 核心数据结构
SLUB 的设计核心在于将管理结构 inline 到页面中:
// include/linux/slub_def.h
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // per-cpu 缓存指针
unsigned long flags; // 对象对齐等标志
unsigned long min_partial; // 最小 partial slab 保留数
unsigned int size; // 对象大小(含元数据)
unsigned int object_size; // 实际对象大小
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点
const char *name; // 缓存名称(/proc/slabinfo 可见)
struct list_head list; // 全局缓存列表
unsigned int offset; // freelist 指针偏移
unsigned int allocflags;
struct kobject kobj; // sysfs 接口
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头
unsigned long tid; // 全局事务 ID(防乱序)
struct page *page; // 当前使用的 slab 页面
struct page *partial; // per-cpu partial 列表
};
SLUB 分配路径中,首先尝试从 cpu_slab->freelist 直接取对象(fast path);如果失败,从 partial 列表获取;如果还不足,向 buddy system 申请新的 slab 页面。
2.3 kmem_cache_create 实战:自定义缓存
在编写内核模块或高性能驱动时,频繁分配同类型的结构体(如网卡驱动中的 sk_buff 自定义扩展)建议创建专用缓存:
// 模块初始化时创建缓存
static struct kmem_cache *my_obj_cache;
static int __init my_init(void)
{
my_obj_cache = kmem_cache_create(
"my_custom_objs", // 缓存名称
sizeof(struct my_object), // 对象大小
0, // 对齐(0 表示自然对齐)
SLAB_HWCACHE_ALIGN, // 硬件缓存对齐标志
NULL // 构造函数(可为 NULL)
);
if (!my_obj_cache)
return -ENOMEM;
return 0;
}
// 使用缓存分配/释放
struct my_object *obj = kmem_cache_alloc(my_obj_cache, GFP_KERNEL);
// ... 使用 obj ...
kmem_cache_free(my_obj_cache, obj);
static void __exit my_exit(void)
{
kmem_cache_destroy(my_obj_cache);
}
2.4 SLUB 调试:使用 slub_debug 排查 UAF
SLUB 内置了强大的调试选项 slub_debug,可以检测 Use-After-Free(UAF)和缓冲区溢出:
// 启动参数添加
slub_debug=FZP
// F - Freepointer check(释放后毒化 freelist 指针)
// Z - Red zoning(对象两端插入不可访问区域)
// P - Poison(释放时用 0x5a 填充对象)
在运行时动态跟踪某个缓存:
# 开启特定缓存的调试
echo 1 > /sys/kernel/slab/kmalloc-24/red_zone
echo 1 > /sys/kernel/slab/kmalloc-24/poison
# 查看分配情况
cat /proc/slabinfo | grep kmalloc
slabtop -o
三、vmalloc:大内存虚拟连续分配
3.1 vmalloc 的适用场景
当需要分配大量虚拟连续但物理可以不连续的内存时(如内核模块加载、io_uring 的 SQ/CQ 环、大页表映射等),使用 vmalloc()。与 kmalloc() 不同,vmalloc 只保证虚拟地址连续,物理地址可能离散。
3.2 vmalloc 的性能陷阱
由于 vmalloc 需要修改页表映射(建立新的 PTE 条目),每次分配/释放都需要 TLB shootdown,这在高 NUMA 系统上代价极高。典型场景:
// 每次 vmalloc 都可能触发 IPI TLB 无效化
void *buf = vmalloc(1 << 20); // 1MB
// 使用 buf...
vfree(buf); // 触发 TLB shootdown
优化建议:对于 io_uring 这类高频操作,优先使用 MAP_POPULATE 的 mmap 或预注册缓冲区(registered buffers),避免每次 I/O 都走 vmalloc。
四、OOM Killer:内存耗尽的最后防线
4.1 触发机制与 oom_badness 评分
当系统内存耗尽且所有回收手段失败时,内核调用 out_of_memory() 选择进程终止。选择算法基于 oom_badness() 函数,评分公式为:
// mm/oom_kill.c
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
// 跳过正在_exit 的进程、内核线程、以及标记为 oom_score_adj 为 -1000 的进程
if (!p->mm || p->flags & PF_KTHREAD || is_global_init(p))
return 0;
if (oom_unkillable_task(p))
return 0;
// 基础分数 = 进程占用内存页数 / 总页数 * 1000
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
// 乘上 oom_score_adj(范围 -1000 ~ 1000)
points = points * 1000 / (unsigned long long)totalpoints;
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN)
return 0;
points += adj;
return points > 0 ? points : 1;
}
4.2 保护关键服务:oom_score_adj
在生产环境中,为了避免关键进程被 OOM Killer 选中,可以通过设置 /proc/[pid]/oom_score_adj 来调整进程的 OOM 评分。值为 -1000 表示永远不会被选中:
# 保护数据库服务(假设 PID 为 1234)
echo -800 > /proc/1234/oom_score_adj
# 对于 systemd 管理的进程,使用 OOMPolicy
# /etc/systemd/system/mysql.service.d/oom.conf
[Service]
OOMPolicy=stop
OOMScoreAdjust=-800
4.3 容器环境的 OOM:cgroup v2 memory.max
在容器化场景下,Docker/Kubernetes 使用 cgroup v2 的 memory.max 限制内存。当容器内存超过阈值时:
- cgroup v1:触发容器内 OOM Killer(选择容器内评分最高的进程)
- cgroup v2:更精确的
memory.peak监控和memory.high软限触发先发回收
# 容器的 cgroup 内存统计
cat /sys/fs/cgroup/memory/docker/<container>/memory.stat
# 关键字段:
#anon - 匿名页(堆内存)
#file - 文件缓存
#kernel_stack - 内核栈内存
#pagetables - 页表内存
#slab_unreclaimable - 不可回收 slab
4.4 OOM 日志解读实战
典型的 OOM Killer 日志如下:
[123456.789] Out of memory: Killed process 12345 (java)
total-vm:8192000kB, anon-rss:3892000kB, file-rss:0kB,
shmem-rss:0kB, UID:1000, oom_score_adj:0
关键信息解读:
- total-vm:进程虚拟地址空间总量
- anon-rss:匿名页驻留集大小(堆+栈)
- file-rss:文件映射驻留集
- oom_score_adj:OOM 评分调整值
五、生产环境调优实战
5.1 透明大页(THP)的取舍
Transparent Huge Pages 使用 2MB 大页代替 4KB 小页,减少 TLB Miss,提升内存密集型应用性能。但可能造成内存碎片化和延迟抖动:
# 查看当前 THP 策略
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never 表示默认开启
# 对于数据库场景(MySQL/PostgreSQL),建议关闭 THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 对于大内存 Java 应用,使用 madvise(仅对标记 MADV_HUGEPAGE 的区域启用)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
5.2 swappiness 与页面回收策略
vm.swappiness 控制系统在内存压力下倾向于回收匿名页(swap)还是文件页(page cache):
# 默认值 60(平衡倾向)
# 0 = 禁用 swap(除非内存极低)
# 100 = 积极 swap
# 对于 SSD 数据库服务器,降低 swappiness 优先回收 page cache
echo 10 > /proc/sys/vm/swappiness
# 对于计算密集型(无 IO 压力),设为 0
echo 0 > /proc/sys/vm/swappiness
5.3 内存超售与 overcommit 策略
Linux 默认启用内存 overcommit(vm.overcommit_memory = 0),允许分配超过物理内存+交换空间的总和,依赖 OOM Killer 在内存耗尽时收拾残局:
# 策略 0(默认):启发式 overcommit
# 策略 1:总是 overcommit(适合科学计算,内存从不用满的场景)
# 策略 2:不允许超过 overcommit_ratio(严格模式)
# 生产环境推荐策略 2(配合合理的 overcommit_ratio)
echo 2 > /proc/sys/vm/overcommit_memory
echo 80 > /proc/sys/vm/overcommit_ratio # 允许分配物理内存的 80%
5.4 NUMA 感知内存分配
在 NUMA 架构服务器上,跨节点访问内存的延迟可达本地节点的 2-3 倍。内核通过 zonelist 控制分配顺序:
# 查看 NUMA 拓扑
numactl --hardware
# 查看进程内存在各 NUMA 节点的分布
numastat -p <PID>
# 将进程绑定到 NUMA 节点 0,优先使用本地内存
numactl --cpunodebind=0 --membind=0 ./my_app
# 使用 mbind() 系统调用在代码中精细控制
set_mempolicy(MPOL_BIND, &nodemask, MAXNODE, addr, len);
六、故障排查工具箱
6.1 排查内存泄漏:slabtop 与 /proc/meminfo
# 实时监控 slab 使用情况(按大小排序)
slabtop -s c
# 查看 /proc/meminfo 关键指标
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached|Slab|PageTables|VmallocTotal"
# 使用 pahole 查看内核结构体大小
pahole task_struct
6.2 页面错误分析:perf probe
# 跟踪 minor/major page fault 频率
perf stat -e page-faults,minor-faults,major-faults -p <PID> sleep 10
# 跟踪 page_fault 内核函数的调用栈
perf probe --add handle_mm_fault
perf record -e probe:handle_mm_fault -a sleep 30
perf script
6.3 直接回收延迟:使用 bpftrace/eBPF
#!/usr/bin/env bpftrace
// 跟踪 direct reclaim 的开始和结束,计算延迟
kprobe:__alloc_pages_direct_compact {
printf("compact: pid=%d comm=%s gfp=%x order=%d\n",
pid, curtask->comm, arg3, arg2);
}
kprobe:direct_reclaim_begin {
@start[tid] = nsecs;
}
kprobe:direct_reclaim_end /@start[tid]/ {
$duration = nsecs - @start[tid];
printf("reclaim: pid=%d comm=%s duration=%d ms reclaimed=%lu pages\n",
pid, curtask->comm, $duration / 1000000, arg1);
delete(@start[tid]);
}
6.4 compaction 效果评估
# 查看 compaction 统计
cat /proc/vmstat | grep -E "compact|compact_
# compact_migrate_scanned - 扫描的可迁移页数
# compact_free_scanned - 扫描的空闲页数
# compact_isolate - 隔离的页面数
# compact_fail - 碎片整理失败次数
# compact_success - 碎片整理成功次数
# 手动触发 compaction
echo 1 > /proc/sys/vm/compact_memory
七、内存管理的未来演进
Linux 内存管理正朝着几个方向持续演进:
- CMT(Coupled Memory Tiering):自动在 DRAM/CXL/PMEM 之间迁移热/冷页面
- MGLRU(Multi-Gen LRU):替代传统 active/inactive 双链表,实现更精确的回收算法
- MSEAL(Memory Sealing):system call 层面提供 VMA 不可变映射支持
- Rust for Linux 内存安全:用 Rust 重写部分内存管理代码路径,减少 UAF、溢出等安全问题
- Folio 大页抽象:统一 compound page 和 head page 的表示,简化 HugeTLB 管理
总结
Linux 内核的内存管理是一个庞大而精密的系统:底层 buddy system 管理物理页、SLUB slab 分配器优化对象分配、OOM Killer 做为最后防线、THP 和大页提升 TLB 命中率、NUMA 策略优化跨节点性能。深入理解这些机制,才能在生产环境中做出正确的调优决策,避免"内存足够但系统卡顿"的陷阱。
对于后端工程师来说,重点掌握 /proc/meminfo 解读、slabtop 监控、OOM 评分调优、THP 策略选择四项基础技能,就能覆盖绝大多数内存相关故障场景。

发表评论 取消回复