引言

Linux 内核的内存管理子系统是整个系统性能的关键基石。当应用程序调用 kmalloc()、kmem_cache_alloc() 或 vmalloc() 时,内核必须在微秒级的延迟内完成内存分配,同时还要应对内存碎片、NUMA 拓扑、并发竞争等复杂挑战。本文将从伙伴系统(Buddy System)的底层算法出发,深入剖析 Slab 与 Slub 分配器的完整实现链路,结合真实生产案例,给出系统性的调优方法论。

一、伙伴系统(Buddy System):物理内存的分配基石

1.1 核心设计思想

伙伴系统是 Linux 内核管理物理内存页的基础算法。它将所有物理页面按 2 的幂次(order)组织成 11 个链表(order 0~10),分别管理 1、2、4、8...1024 个连续物理页框。分配时向上取整到最近的 2^n,若该 order 无空闲则从更大 order 递归拆分;释放时检查"伙伴"(地址相邻且大小相同)是否空闲,若空闲则向上合并。

1.2 关键数据结构

// mmzone.h
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

struct zone {
    struct free_area free_area[MAX_ORDER];  // 11 个 orders
    ...
};

每个内存域(Zone)维护一组 free_area,每种迁移类型(MIGRATE_UNMOVABLE、MIGRATE_MOVABLE、MIGRATE_RECLAIMABLE 等)独立管理,从源头减少碎片。

1.3 分配路径详解

alloc_pages() 是伙伴系统的入口函数。其执行路径为:alloc_pages() → alloc_pages_node() → __alloc_pages_nodemask()。后者依次经过快速路径(get_page_from_freelist,基于 per-CPU hot page cache)、慢速路径(__alloc_pages_slowpath,触发 compaction/reclaim)和 OOM 兜底(out_of_memory())。

1.4 页面迁移与反碎片

物理页按迁移类型分类,将不可移动页(kernel 线性映射)与可移动页(用户态匿名页、文件缓存)隔离。内存紧缩(compaction)阶段可回收/迁移可移动页,整理出连续大块内存。相关 sysctl 参数:vm.extfrag_threshold(外部碎片阈值,默认 500)、vm.min_free_kbytes(最小保留内存)。

二、Slab 分配器:小对象高性能分配

2.1 为什么需要 Slab

伙伴系统以页(通常 4KB)为单位分配,而内核中大量数据结构(task_struct、dentry、inode、file 等)远小于一页。若直接用伙伴系统,内部碎片严重(如 96 字节的对象放在 4096 字节的页中,浪费 97%),且频繁分配/释放带来高昂的初始化/销毁开销。Slab 分配器正是为这一问题而生。

2.2 三层架构

Slab 采用 kmem_cache → slab → object 的三层结构:

  • kmem_cache:描述一种对象类型的缓存,记录对象大小、对齐、构造函数、着色偏移等信息。系统启动时会为常用内核对象自动创建对应的 cache(如 task_struct_cachep、dentry_cache)。
  • slab:每个 slab 占一页或多页连续内存,内部等分为 N 个对象槽位。Slab 有三种状态:full(全部分配)、partial(部分分配)、free(全空闲)。
  • object:实际分配给调用者的内存单元,有 inuse 和 free 两种状态。

2.3 kmem_cache 创建与销毁

// 创建专用 cache
struct kmem_cache *kmem_cache_create(
    const char *name,        // cache 名称
    size_t size,             // 对象大小
    size_t align,            // 对齐要求
    unsigned long flags,     // SLAB_HWCACHE_ALIGN 等
    void (*ctor)(void *)     // 构造函数
);

// 销毁 cache(确保无活跃对象)
void kmem_cache_destroy(struct kmem_cache *cachep);

2.4 分配与释放路径

kmem_cache_alloc() 流程:

  1. 检查 per-CPU CPU slab cache(热路径,无锁)
  2. 从 kmem_cache_node 的 partial 链表取 slab
  3. partial 空则向伙伴系统请求新页面,初始化 slab
  4. 返回对象指针(free 列表中弹出)

kmem_cache_free() 反向操作:对象归还到 slab 的 free 列表,若 slab 从 partial 变为 empty 则归还给 node 管理,可释放回伙伴系统。

2.5 缓存着色(Cache Coloring)

现代 CPU L1/L2 cache 使用 set-associative 映射。若大量 slab 起始地址相同偏移的对象被访问,会造成 cache line 冲突(cache thrashing)。着色机制在每个 slab 初始化时加入随机的 coloroff 偏移,使不同 slab 中同序号对象映射到不同 cache set。相关字段:kmem_cache.colour_off(每次递增 cache line 大小)、kmem_cache.colour(颜色总数)。

三、Slub 分配器:Slab 的继任者

3.1 Slub 解决的核心痛点

随着 NUMA 系统和核心数增长,经典 Slab 暴露三个问题:

  • 每个 node 的 partial 链表在无 NUMA 系统中无法利用本地节点优势,链表操作开销大
  • 调试代码(如 redzone、poison)增加元数据开销,导致对齐和着色复杂化
  • 代码可维护性差,多种分配路径条件分支交织(SLOB、SLAB、Slub 共存)

Slub(Unqueued Slab)从 2.6.23 起作为默认分配器,去掉了 Slab 的复杂队列机制,简化为"CPU 本地 slab + node partial 链表"的两层结构。

3.2 Slub 核心数据结构

struct kmem_cache_cpu {
    void **freelist;        // 指向下一个空闲对象
    unsigned long tid;      // 全局唯一事务 ID(防止 ABA)
    struct page *page;      // 当前 CPU 使用的 slab 页
    struct page *partial;   // CPU local partial 链表(v5.9+)
};

struct kmem_cache_node {
    spinlock_t list_lock;
    unsigned long nr_partial;
    struct list_head partial;  // node 级 partial slab 链表
};

3.3 Redzone 与 Poison 机制

当开启 CONFIG_DEBUG_SLAB 时,Slub 在对象边界放置 redzone 标记(0x12345678 模式),释放时校验是否被踩。POISON 在释放时用固定模式(如 0x5a)填充对象内存,使 use-after-free 即时暴露。

3.4 Slub 分配流程图

slab_alloc() 执行流程:

  1. 读 per-CPU freelist(无锁快速路径)
  2. freelist 空但 page->partial 非空 → 从 local partial 取 slab
  3. local partial 空 → 加 node 锁,从 node->partial 链表移动一个 slab 到 CPU
  4. node 也没 partial → 释放锁,向伙伴系统请求新页面(new_slab()),获得后初始化 freelist
  5. 返回对象指针并更新 freelist(指针跳转到对象内嵌入的 next 指针)

四、生产环境实战:诊断与调优

4.1 读懂 /proc/slabinfo

$ cat /proc/slabinfo | head -20
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables ...
dentry             1204567  1589920     192   20    2 : tunables ...
inode_cache         982341  1105080     648    5    2 : tunables ...
kmalloc-64          452312   512000      64   62    1 : tunables ...

关键字段含义:active_objs 已分配对象数、num_objs 总对象数(active + free)、objsize 对象大小、objperslab 每 slab 对象数、pagesperslab 每 slab 占用页数。碎片率 = 1 - active_objs / num_objs,若接近 1 说明该 cache 存在大量未使用对象。

4.2 slabtop 实时监控

sudo slabtop -o --sort=a
 OBJS ACTIVE  USE OBJ  SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
 512k   512k 100%   0.00K  128       4     128k kmalloc-128
 320k   298k  93%   0.19K   60      10     240k dentry
 256k   220k  86%   0.25K   32       8     256k inode_cache

4.3 Reaper 与主动回收

内核线程 kwork_reaper 通过 kmem_cache_shrink() 扫描所有 cache 的空闲 slab 并归还伙伴系统。Slub 引入"周期性 slab 老化"机制:slab_alloc_node() 分配失败时检查 node->nr_partial,若 min_partial 较大则触发回收。关键参数:

  • vm.slub_min_objects:node partial 最低水位线下限
  • vm.slab_ratio:每个 cache 应分配的 page 数量与 node 总内存比例
  • min_partial:node 维护的最小 partial slab 数,默认值与 node 内存大小成正比

4.4 OOM 场景分析

当伙伴系统所有 order 都无空闲且回收也无法满足时,触发 OOM killer。OOM 评分算法(oom_badness())考虑进程占用内存、运行时间、oom_score_adj、是否为 init 进程等。Slab 泄漏是常见诱因:若某 cache 持续分配却不释放(如未关闭的 fd、未清理的 timer),会导致num_objs - active_objs 持续增加,进而耗尽某 zone 内存。

4.5 常见内存碎片处理

# 查看碎片指数
cat /proc/buddyinfo
cat /proc/pagetypeinfo

# 手动触发内存紧凑
echo 1 > /proc/sys/vm/compact_memory

# 调整 compaction 激进程度
sysctl -w vm.extfrag_threshold=100   # 更积极(默认 500)
sysctl -w vm.zone_reclaim_mode=1     # NUMA 开启本地回收

五、Slab 与 Slub 关键差异总结

维度SlabSlub
队列复杂度每 CPU + 每 node 三级队列(full/partial/empty)两层:CPU slab + node partial 链表
调试开销元数据在 slab 外部,coloring 与 redzone 交织元数据嵌入页结构,统一 redzone + poison
NUMA 友好度empty slab 分配到 node 可能不均衡本地 node 优先 + fallback 策略更明确
默认启用2.6.23 之前的默认分配器2.6.23 至今的默认分配器(x86/ARM)
代码体量~5000 行~2000 行(更简洁)

六、性能调优实践建议

  1. 监控碎片率:通过 slabtop -s c 按缓存大小排,若某 cache 碎片率持续 > 50%,排查对应的内核泄漏点。
  2. 合理设置 min_free_kbytes:HPC / 数据库场景建议物理内存的 2%~5%,保证 compaction 和紧急分配有足够预留。
  3. 开启 compaction:大块内存分配频繁的场景(DPDK、GPU 直通),建议设为 vm.extfrag_threshold=100。
  4. 关闭不必要的调试:生产环境不要使用 slab_debug=FPZU,每个对象增加 ~40 字节开销。
  5. NUMA 感知:关键线程绑核 + mbind() 本地分配,减少跨 node 访问。配合 vm.zone_reclaim_mode=1 和 numactl --localalloc。
  6. dentry 缓存控制:海量文件操作场景,定期 sync && echo 3 > /proc/sys/vm/drop_caches 清理 dentry/inode cache,避免 slab 膨胀。

七、前沿演进:SLUB-TINY 与 Rust 抽象

针对嵌入式/小内存场景,SLUB-TINY(~Linux 6.4)将对象级元数据进一步压缩,省去 free list pointer,改用 slab 内偏移计算。Linux 6.x 开始,Rust 模块可利用 SlabAllocator trait 为驱动提供类型安全的 cache 分配接口。随着 CXL.mem 和 tiered memory 的引入,Slub 的 node fallback 链表也将扩展为"DRAM → CXL tier"的多层结构。

八、总结

伙伴系统通过 2^n 阶连续页面管理物理内存,是内核内存的底层银行。Slab/Slub 在其上构建对象级分配池,通过着色、per-CPU hot cache 和延缓归还等策略实现微秒级分配延迟、高 cache 命中率与低碎片化的平衡。深入理解这套机制,是性能调优、故障排查和内核开发的必经之路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }