引言

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 性能对比

指标kmallocvmalloc
物理连续性✅ 连续❌ 离散
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 三层架构的设计哲学和工作原理,不仅能帮助我们诊断生产环境问题,更能指导我们写出内存友好的内核代码。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部