4. slab 创建与销毁:kmem_cache_create、kmem_cache_destroy、构造函数与析构函数

slab 分配器的核心操作围绕 缓存(cache) 的生命周期展开。内核通过 kmem_cache_create 创建一个专用缓存,使用完毕后通过 kmem_cache_destroy 销毁。在此过程中,构造函数(constructor)和析构函数(destructor)提供了对象初始化和清理的钩子。本节将深入剖析这些接口的实现机制、参数语义以及工程陷阱。

4.1 kmem_cache_create:缓存创建的全流程

kmem_cache_create 是 slab 分配器的“工厂函数”。它接收对象大小、对齐方式、标志位以及构造/析构函数,返回一个 struct kmem_cache 指针。其原型如下:

struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size,
                  unsigned int align, slab_flags_t flags,
                  void (*ctor)(void *));

4.1.1 参数语义与对齐规则

参数 含义 典型值 / 注意事项
name 缓存名称,用于 /proc/slabinfo 标识 建议使用模块名+对象名,如 "nf_conntrack"
size 每个对象的大小(字节) 必须大于 0;内核会向上对齐到 sizeof(void *)
align 对象对齐字节数 0 表示使用默认对齐(L1 缓存行对齐);通常传入 ARCH_KMALLOC_MINALIGN
flags 控制 slab 行为的标志位 SLAB_HWCACHE_ALIGN、SLAB_PANIC、SLAB_ACCOUNT 等
ctor 对象构造函数,每次分配新对象时调用 可为 NULL;不可在原子上下文中使用复杂操作
对齐工程建议: 对于网络包描述符、文件系统 inode 等频繁访问的结构体,务必使用 SLAB_HWCACHE_ALIGN 标志,避免伪共享(false sharing)导致的性能抖动。

4.1.2 内部创建流程

当调用 kmem_cache_create 时,内核执行以下关键步骤:

  1. 参数校验与调整: 检查 size 是否超过 KMALLOC_MAX_SIZE,对齐值是否合法。
  2. 计算 slab 阶数: 根据对象大小和缓存行大小,确定每个 slab 包含的页面数(gfporder)。
  3. 分配 kmem_cache 结构体: 从 kmem_cache_boot 或通用缓存中分配管理结构。
  4. 初始化 per-CPU 缓存: 为每个 CPU 分配 array_cache,设置批量转移阈值。
  5. 注册到全局链表: 将新缓存加入 slab_caches 链表,并更新 /proc/slabinfo 接口。
关键数据结构关系: 每个 kmem_cache 内部维护三个 slab 链表:slabs_full(全部分配)、slabs_partial(部分分配)、slabs_free(全部空闲)。分配时优先从 slabs_partial 获取对象。
kmem_cache_create 内部创建流程 参数校验与对齐调整 计算 slab 阶数 分配 kmem_cache 结构 初始化 per-CPU array_cache 创建 slab 着色表 注册到 slab_caches 链表 返回 struct kmem_cache *

4.2 kmem_cache_destroy:安全销毁的艺术

销毁一个 slab 缓存比创建更危险。内核必须确保:

  • 无泄漏: 所有已分配对象必须在使用者调用 kmem_cache_free 后归还。
  • 无并发: 销毁期间不能有新的分配或释放操作。
  • 无残留: 所有 slab 页面必须释放回伙伴系统。

4.2.1 销毁流程

void kmem_cache_destroy(struct kmem_cache *s)
{
    // 1. 检查缓存是否为空(所有对象已释放)
    if (!s->num_objects)
        goto destroy;

    // 2. 遍历所有 slab,释放空闲 slab 页面
    // 3. 释放 per-CPU 缓存
    // 4. 从全局链表移除
    // 5. 释放 kmem_cache 结构体
}
常见错误: 如果模块卸载时忘记调用 kmem_cache_destroy,会导致 slabinfo 中出现孤儿缓存,长期运行后造成内存泄漏。建议在模块退出路径中使用 kmem_cache_destroy 配合引用计数保护。

4.2.2 销毁失败场景

场景 内核行为 调试手段
缓存非空(仍有对象未释放) 打印警告并返回 -EBUSY 检查 /proc/slabinfo 中该缓存的 active_objs
缓存已被销毁 触发 BUG_ON 或空指针解引用 使用 kmem_cache_destroy 前设置 NULL 检查
并发分配/释放 可能导致 slab 链表损坏 确保调用者持有模块互斥锁

4.3 构造函数与析构函数:对象生命周期的钩子

构造函数(constructor)和析构函数(destructor)是 slab 分配器提供的两个可选回调,用于在对象分配和释放时执行自定义初始化/清理。

4.3.1 构造函数语义

构造函数在 首次分配 对象时调用,后续从 slab_free 链表复用对象时 不会 再次调用。这意味着构造函数只负责“冷启动”初始化,不负责每次分配后的重置。

void my_ctor(void *obj)
{
    struct my_struct *p = (struct my_struct *)obj;
    memset(p, 0, sizeof(*p));      // 清零
    spin_lock_init(&p->lock);      // 初始化锁
    INIT_LIST_HEAD(&p->list);      // 初始化链表头
}
性能陷阱: 构造函数在 slab 分配页面时批量调用,如果构造函数执行耗时操作(如 kmalloc 或 mutex_lock),会严重拖慢 slab 页面分配速度。构造函数应只做轻量级初始化。

4.3.2 析构函数语义

析构函数在对象被释放回 slab 时调用。与构造函数不同,析构函数 每次 释放都会执行。它用于清理外部资源(如释放子缓冲区、断开引用计数)。

void my_dtor(void *obj)
{
    struct my_struct *p = (struct my_struct *)obj;
    if (p->buffer) {
        kfree(p->buffer);          // 释放内部缓冲区
        p->buffer = NULL;
    }
}
注意: 内核主线版本中,kmem_cache_create 的 dtor 参数已被移除(从 2.6.22 开始)。如果需要析构功能,建议在调用 kmem_cache_free 前由调用者手动清理,或使用 SLAB_TYPESAFE_BY_RCU 标志配合 RCU 机制。

4.3.3 构造函数 vs 析构函数对比

特性 构造函数 析构函数
调用时机 对象首次分配(slab 页面创建时) 每次对象释放
调用频率 低(仅当分配新 slab 页面时) 高(每次 kmem_cache_free)
典型用途 清零、初始化锁/链表 释放子资源、解除引用
性能影响 影响 slab 页面分配延迟 影响释放路径延迟
内核支持现状 完全支持 已移除(需手动实现)
slab 对象生命周期:构造函数与析构函数 slab 页面分配 (伙伴系统) 构造函数批量调用 (每个对象一次) 对象分配/释放 (多次复用) slab 页面回收 (伙伴系统) 关键语义说明 • 构造函数仅在 slab 页面首次分配时调用,后续对象复用不触发 • 析构函数(已移除)每次释放都调用;现代内核需手动在 free 前清理

4.4 工程实战:自定义 slab 缓存示例

以下代码展示了一个完整的 slab 缓存创建、使用和销毁的典型模式:

#include <linux/slab.h>
#include <linux/module.h>

struct my_obj {
    int id;
    char name[32];
    struct list_head node;
    spinlock_t lock;
};

static struct kmem_cache *my_cachep;

static int __init my_module_init(void)
{
    // 创建缓存:对象大小 48 字节,硬件缓存行对齐,带构造函数
    my_cachep = kmem_cache_create("my_obj_cache",
                                  sizeof(struct my_obj),
                                  0,
                                  SLAB_HWCACHE_ALIGN,
                                  my_ctor);
    if (!my_cachep)
        return -ENOMEM;
    return 0;
}

static void __exit my_module_exit(void)
{
    struct my_obj *obj;
    int leaked = 0;

    // 销毁前检查泄漏(调试用)
    while ((obj = kmem_cache_alloc(my_cachep, GFP_KERNEL)) != NULL) {
        leaked++;
        kmem_cache_free(my_cachep, obj);
    }

    kmem_cache_destroy(my_cachep);
    if (leaked > 0)
        pr_warn("my_cache: recovered %d leaked objects\n", leaked);
}

module_init(my_module_init);
module_exit(my_module_exit);
调试技巧: 在 kmem_cache_destroy 前主动分配并释放所有对象,可以检测调用者是否忘记释放。配合 CONFIG_SLUB_DEBUG 开启的 slabinfo 工具,可以精确追踪每个缓存的分配来源。

4.5 常见陷阱与最佳实践

  • 陷阱1:构造函数中调用 kmalloc —— 构造函数在 slab 分配器持有锁时调用,kmalloc 可能触发递归分配导致死锁。解决方案:使用 GFP_ATOMIC 或预分配资源池。
  • 陷阱2:销毁时未同步 —— 多线程环境下,一个线程正在分配对象,另一个线程调用 kmem_cache_destroy。解决方案:使用引用计数或 RCU 保护缓存生命周期。
  • 陷阱3:对齐参数错误 —— 对于 DMA 缓冲区,对齐必须满足硬件要求(如 64 字节对齐)。错误对齐会导致 DMA 传输失败或性能下降。
  • 最佳实践: 始终在模块退出路径中调用 kmem_cache_destroy,并使用 SLAB_PANIC 标志让内核在创建失败时直接 panic,避免空指针传播。