4. Slab 分配与释放流程:从 kmem_cache_alloc 到 slab 分配,以及 kmem_cache_free 的完整路径

Slab 分配器的核心价值在于为内核对象提供高速、低碎片化的内存管理。理解 kmem_cache_alloc 和 kmem_cache_free 的完整执行路径,是进行性能调优和问题定位的基础。本章将深入剖析从 API 调用到物理内存操作的每一层逻辑。

4.1 分配路径:kmem_cache_alloc 的逐层穿透

当内核模块调用 kmem_cache_alloc(cachep, flags) 时,分配器会按照“局部缓存 → 共享缓存 → 伙伴系统”的优先级顺序尝试获取对象。整个路径可抽象为以下三层结构:

核心原则: 分配路径优先使用 CPU 本地缓存(L1 cache),避免锁竞争;仅在本地缓存耗尽时,才向全局共享缓存或伙伴系统申请新的 slab 页。
kmem_cache_alloc 完整分配路径流程图 kmem_cache_alloc Step 1: 检查 CPU 本地空闲对象链表 (array_cache - 无锁或本地锁) 有可用对象? 返回对象 Step 2: 从共享缓存 (slab_shrink) 转移 (批量转移对象到本地缓存) 成功? 返回对象 Step 3: 从伙伴系统分配新 slab (alloc_pages / __slab_alloc)

4.2 分配路径关键数据结构

在分配路径中,有三个核心数据结构决定了性能表现:

数据结构 作用域 锁粒度 性能特征
array_cache Per-CPU 无锁 / 本地中断屏蔽 最快,L1 cache 友好
kmem_cache_node->shared NUMA 节点 自旋锁 中等,批量转移减少锁竞争
slab 描述符 + 伙伴系统 全局 全局锁 / 页分配锁 最慢,涉及页分配和内存清零
调优要点: 通过调整 batchcount 和 limit 参数,可以控制 array_cache 与共享缓存之间的传输粒度。增大 batchcount 可减少共享缓存访问次数,但会增加本地缓存占用。

4.3 释放路径:kmem_cache_free 的逆向操作

释放路径是分配路径的逆过程,同样遵循“先本地、后全局”的策略。当调用 kmem_cache_free(cachep, objp) 时,执行以下步骤:

  1. 检查对象所属 slab: 通过 virt_to_head_page 或 slab 内部指针,找到对象所在的 slab 描述符。
  2. 尝试放回本地缓存: 如果当前 CPU 的 array_cache 未满(未达到 limit),直接将对象放入空闲链表,释放完成。
  3. 缓存溢出处理: 如果本地缓存已满,则将本地缓存中的一批对象(batchcount 个)批量转移回共享缓存或 slab 的空闲链表。
  4. Slab 回收判断: 如果 slab 中所有对象都已释放,且满足回收条件(如 slab_reclaim 标志),则将该 slab 页面归还给伙伴系统。
kmem_cache_free 释放路径流程图 kmem_cache_free Step 1: 定位对象所属 slab Step 2: 放入 CPU 本地缓存 缓存满? 释放完成 Step 3: 批量转移回共享缓存 / slab

4.4 关键代码路径解析

以下代码展示了分配路径中最核心的本地缓存快速路径。理解这段逻辑有助于定位性能瓶颈:

// mm/slub.c (简化版)
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags) {
    struct kmem_cache_cpu *c;
    void *object;

    // 1. 获取当前 CPU 的本地缓存
    c = raw_cpu_ptr(s->cpu_slab);

    // 2. 尝试从本地空闲链表获取对象 (快速路径)
    object = c->freelist;
    if (likely(object)) {
        c->freelist = get_freepointer(s, object);
        c->pages--;
        // 统计本地命中
        this_cpu_inc(s->cpu_slab->local_hits);
        return object;
    }

    // 3. 快速路径失败,进入慢速路径
    return __slab_alloc(s, gfpflags, c);
}

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags,
                          struct kmem_cache_cpu *c) {
    // 4. 尝试从共享缓存 (node->shared) 批量转移
    // 5. 如果共享缓存也空,则从伙伴系统分配新 slab 页
    // 6. 初始化 slab 并建立空闲链表
    // 7. 返回第一个对象
}
性能陷阱: 如果 kmem_cache_alloc 频繁进入 __slab_alloc 慢速路径,说明本地缓存容量不足或 batchcount 设置过小。此时应检查 /proc/slabinfo 中的 cpu_objs 和 limit 字段。

4.5 分配与释放路径对比总结

维度 分配路径 (alloc) 释放路径 (free)
快速路径 从 array_cache->freelist 取对象 放入 array_cache->freelist
慢速路径触发条件 本地缓存为空 本地缓存已满 (达到 limit)
慢速路径操作 从共享缓存或伙伴系统获取 slab 批量转移对象到共享缓存或释放 slab
锁竞争风险 低 (快速路径无锁) 低 (快速路径无锁)
主要开销 缓存未命中时的页分配 缓存满时的批量转移和可能的页释放

4.6 调优指标与监控方法

通过以下指标可以评估分配/释放路径的健康状况:

  • 本地命中率 (local_hits / total_alloc): 理想值应 > 95%。低于此值说明本地缓存配置不足。
  • Slab 页分配速率: 通过 /proc/vmstat 中的 slab_alloc 和 slab_free 监控。速率突增可能暗示内存碎片或对象泄漏。
  • 平均分配延迟: 使用 ftrace 或 perf 测量 kmem_cache_alloc 的延迟分布。快速路径应在纳秒级,慢速路径可能达到微秒级。
实战命令:
# 查看 slab 分配统计信息
cat /proc/slabinfo | grep -E "kmalloc-|task_struct"
# 使用 perf 统计分配热点
perf stat -e kmem:kmem_cache_alloc,kmem:kmem_cache_free -a sleep 5