引言
Linux 内核的内存管理子系统是整个操作系统最核心、最复杂的组件之一。它不仅负责物理页面的分配与回收,还要在性能、安全性和内存碎片之间做出精细的权衡。本文将深入剖析 Linux 内核内存管理的三大基石:Buddy 分配器、SLAB/SLUB 分配器以及 vmalloc 机制,并给出生产环境中的实战调试方法。
一、Buddy 分配器:物理页的管理中枢
1.1 核心设计理念
Buddy 分配器管理物理内存的最小单位是页(通常 4KB)。它将物理内存组织为 11 个空闲链表(order 0 到 order 10),分别对应 1、2、4、8...1024 个连续页框。这种二幂分层的结构使得分配和释放都能在对数时间内完成。
// 核心数据结构:free_area
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
// 分配 2^order 个页面的核心路径
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
// 1. 从对应 order 的空闲链表取页面
// 2. 若不足,从更高阶链表"分裂"获得
// 3. 使用伙伴算法(buddy)合并相邻空闲块
}
1.2 伙伴算法的合并与分裂
当两个相邻的 2^order 页框同时空闲时,它们会被合并为 2^(order+1) 的更大块。这里的"伙伴"关系通过页框号(page frame number)的按位异或实现:buddy_pfn = pfn ^ (1 << order)
1.3 页面迁移类型(Migrate Types)
为解决物理内存碎片问题,Linux 2.6.24 引入了可回收页面迁移机制。每个 free_area 内部按迁移类型分组:
- MOVABLE:用户空间页面,可通过页迁移回收
- RECLAIMABLE:内核缓存(如 slab),可通过 shrinker 回收
- UNMOVABLE:内核数据结构(如 task_struct),不可移动
二、SLUB 分配器:内核对象的专用缓存
2.1 为什么需要 SLAB
内核频繁创建销毁大量固定大小的结构体(如 task_struct、inode、dentry),若每次通过 Buddy 分配器走完整路径,会浪费大量 CPU 在初始化和对象跟踪上。SLAB 分配器通过预分配对象缓存池来解决这个问题。
2.2 SLUB 的核心优势
SLUB(Unqueued Slab Allocator)作为 Linux 默认的 SLAB 实现,相比传统 SLAB:
- 去除了每 CPU 队列和 NUMA 节点队列的复杂管理
- 使用 page->freelist 链表直接管理空闲对象,减少元数据开销
- 更少的内存碎片和更好的可扩展性
// SLUB 核心结构
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // 每CPU热路径
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA节点
unsigned int size; // 对象大小(含对齐)
unsigned int object_size; // 实际对象大小
unsigned int offset; // 下一个空闲对象的偏移
struct list_head list; // 全局缓存链表
};
// 快速分配路径(无锁)
static void *slab_alloc(struct kmem_cache *s, gfp_t gfp)
{
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
void *freelist = c->freelist;
if (likely(freelist)) {
c->freelist = get_freelist(freelist); // O(1)
c->page->inuse++;
return freelist;
}
// 慢路径:从 partial 列表或新页分配
return ___slab_alloc(s, gfp);
}
2.3 kmem_cache 的创建与销毁
// 创建专用缓存
struct kmem_cache *task_struct_cachep = kmem_cache_create(
"task_struct",
sizeof(struct task_struct),
0, // align
SLAB_PANIC | SLAB_ACCOUNT, // flags
NULL // ctor
);
// 分配对象
struct task_struct *tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
// 释放对象
kmem_cache_free(task_struct_cachep, tsk);
三、kmalloc vs vmalloc:一级分配器的选择
3.1 kmalloc:物理连续映射
kmalloc 返回的虚拟地址与物理地址存在直接映射(offset),最大通常 4MB-8MB(由 MAX_ORDER 和映射空间限制)。它适合:
- DMA 操作(需要物理连续内存)
- 小对象的频繁分配释放
- 对性能要求极高的热路径
kmalloc 内部通过 kmem_cache 实现:内核预建了 13 个通用缓存(8B 到 32MB),按 2 的幂次对齐,分配时选择能满足要求的最小缓存。
3.2 vmalloc:虚拟连续、物理离散
vmalloc 只保证虚拟地址连续,物理页通过 alloc_page 逐一分配。每次映射需要修改页表,因此 TLB 压力大,不适合频繁访问的场景。
3.3 性能对比
| 指标 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | ✅ 连续 | ❌ 离散 |
| TLB 效率 | 高(大块用 huge page) | 低(逐页映射) |
| 最大 allocation | ~4MB(受 MAX_ORDER 限制) | 接近物理内存总量 |
| 分配延迟 | O(1) 快速路径 | 需修改页表,高延迟 |
| 适用场景 | DMA、小对象 | 大容量缓冲区、模块加载 |
四、内存碎片与反碎片策略
4.1 外部碎片问题
长时间运行后,内存被不同迁移类型的页面交错占据,导致无法分配高阶连续块。典型场景:数据库运行数月后,OOM 突然爆发。
4.2 主动规整(Memory Compaction)
Linux 引入 compaction 机制,通过扫描 MOVABLE 页面并将其迁移,腾出连续物理空间:
// compact_zone 核心流程
1. 扫描 zone 中所有 page block
2. 分离 MOVABLE 页(migratepages 列表)
3. 寻找空闲目标页面
4. 执行页面迁移(更新页表和映射)
5. 合并形成高阶空闲块
4.3 透明大页(THP)
transparent_hugepage 通过预分配 2MB/1GB 大页减少 TLB miss,但对数据库/Redis 可能带来突发延迟,建议对延迟敏感场景关闭。
五、NUMA 感知内存分配
多路服务器中,不同 CPU 到内存控制器的延迟差异可达 2-3 倍。Linux 通过本地节点优先策略缓解此问题:
- 默认从请求 CPU 所在的本地节点分配
- 本地不足时按节点距离(distance)阶梯回退
- 可通过
numactl --membind或set_mempolicy()强制策略 - SLUB 实现中每个 node 维护独立的 partial 列表
六、实战调试与监控
6.1 快速诊断内存压力
# 查看伙伴分配器状态
cat /proc/buddyinfo
# Node 0, zone Normal 100 200 150 80 40 20 10 5 2 1 0
# 查看 Slab 占用
cat /proc/slabinfo | head -20
slabtop # 实时 slab 监控
6.2 OOM 诊断
# 查看 OOM 触发历史
dmesg | grep -i "out of memory"
journalctl -k | grep oom-killer
# 关键参数调优
/proc/sys/vm/overcommit_memory # 0=启发式 1=总是允许 2=严格
/proc/sys/vm/overcommit_ratio # 模式下允许的内存百分比
6.3 slab 泄漏检测
当 slab 占用持续增长但不释放时,说明存在对象泄漏:
# 开启 slab 调试
slub_debug=FZP # 开启 Poison/RedZoning/Tracking
内核会在对象前后放置特定魔数(0x5a5a5a5a 和 0x6b6b6b6b),越界写出时立即触发 kernel panic 并打印调用栈。
七、总结与最佳实践
- 小对象高频场景优先复用 kmem_cache,避免走通用 kmalloc
- 大容量缓冲区用 vmalloc,但注意 TLB 影响
- 数据库/内存数据库场景关闭 THP,减少大页分配延迟
- 容器场景通过 cgroup v2 memory.max 硬限制,避免 OOM 级联
- 实时监控 /proc/buddyinfo 和 slabtop,提前预警碎片化
- 内核升级关注 SLUB 行为变化(如 v6.x 引入的优先级回收)
Linux 内核内存管理是一个经过数十年演进的精巧系统。理解 Buddy、SLUB、vmalloc 三层架构的设计哲学和工作原理,不仅能帮助我们诊断生产环境问题,更能指导我们写出内存友好的内核代码。

发表评论 取消回复