4. 常见泄漏模式

内存泄漏并非单一现象,在内核驱动开发中,泄漏模式通常可以归纳为四种典型类型。理解这些模式是使用 kmemleak 进行高效排查的前提。本节将逐一剖析每种模式的成因、代码特征与排查要点。

四种常见内核内存泄漏模式 分配后未释放 kmalloc/kzalloc 无对应 kfree 最基础、最常见 直接丢失指针 错误路径未释放 if/switch 分支 goto 跳过释放 异常处理遗漏 条件编译导致 引用计数泄漏 kref / get/put 计数永不归零 对象无法释放 模块卸载残留 缓存对象泄漏 kmem_cache_alloc slab 对象未归还 内存池膨胀 碎片化加剧 提示:实际驱动中,四种模式常混合出现,kmemleak 可统一追踪

4.1 分配后未释放(直接泄漏)

这是最原始、最容易理解的内存泄漏模式。驱动在某个执行路径上通过 kmalloc()、kzalloc()、alloc_pages() 等接口分配了内存,但在后续流程中完全没有对应的 kfree()、free_pages() 释放操作。分配的内存指针要么被覆盖,要么超出作用域后丢失,导致该内存块永久驻留。

典型代码特征:
  • 分配函数与释放函数数量不匹配(分配次数 > 释放次数)
  • 指针被重新赋值前未释放旧内存
  • 函数提前 return 时遗漏释放
// 错误示例:分配后未释放
static int __init my_driver_init(void)
{
    char *buf;

    buf = kmalloc(1024, GFP_KERNEL);
    if (!buf)
        return -ENOMEM;

    // 使用 buf 进行一些操作...

    // 错误:忘记调用 kfree(buf);
    // 函数返回后,buf 指针丢失,1024 字节永久泄漏

    return 0;
}
module_init(my_driver_init);
排查技巧: kmemleak 对这种模式检测最直接——未释放的内存块会以 "unreferenced object" 形式出现在 /sys/kernel/debug/kmemleak 中,并附带完整的分配栈回溯。

4.2 错误路径未释放(条件泄漏)

这种模式比直接泄漏更隐蔽。驱动在正常路径上正确地释放了内存,但在错误处理分支(如 if 判断失败、goto 跳转、异常提前返回)中遗漏了释放操作。常见于具有多层资源获取的初始化函数中。

错误路径泄漏:资源获取与释放流程 分配资源 A 分配资源 B 正常释放 A+B 资源 B 分配失败 泄漏:A 未释放 关键点:错误路径中,已成功获取的资源 A 必须显式释放,否则形成泄漏
// 错误示例:错误路径未释放
static int my_probe(struct platform_device *pdev)
{
    char *buf_a, *buf_b;

    buf_a = kmalloc(512, GFP_KERNEL);
    if (!buf_a)
        return -ENOMEM;

    buf_b = kmalloc(512, GFP_KERNEL);
    if (!buf_b) {
        // 错误:这里直接返回,没有释放 buf_a
        // 导致 512 字节泄漏
        return -ENOMEM;
    }

    // 正常使用...

    kfree(buf_b);
    kfree(buf_a);
    return 0;
}
避坑指南: 内核社区推荐使用 goto 统一出口模式来避免此类泄漏。在函数末尾设置标签,所有错误路径都 goto out_free_a、goto out_free_b,确保每个已分配资源都有对应的释放点。

4.3 引用计数泄漏

内核中许多对象(如 struct kref、设备模型中的 struct device、文件系统中的 struct inode)通过引用计数管理生命周期。每次 kref_get()、get_device()、kobject_get() 增加计数,对应的 kref_put()、put_device()、kobject_put() 减少计数。当计数无法归零时,对象的析构函数不会被调用,导致对象及其附属内存永久泄漏。

引用计数接口 增加操作 减少操作 典型泄漏场景
kref kref_get() kref_put() 异步回调中多了一次 get,忘记 put
struct device get_device() put_device() 设备指针被多处引用,释放路径不完整
kobject kobject_get() kobject_put() sysfs 创建后未配对删除
module try_module_get() module_put() 模块引用计数泄漏导致无法卸载
// 引用计数泄漏示例
static struct my_obj {
    struct kref refcount;
    int data;
} *g_obj;

static void obj_release(struct kref *ref)
{
    struct my_obj *obj = container_of(ref, struct my_obj, refcount);
    kfree(obj);  // 只有 kref_put 使计数归零时才会调用
}

static void callback(void *arg)
{
    struct my_obj *obj = (struct my_obj *)arg;
    // 错误:每次回调都增加引用,但从未减少
    kref_get(&obj->refcount);
    // 使用 obj...
    // 缺少 kref_put(&obj->refcount, obj_release);
}

// 结果:g_obj 的引用计数持续增长,永远无法释放
排查技巧: 引用计数泄漏通常不会直接显示为 kmemleak 中的 "unreferenced object",而是表现为对象仍然被引用。可以检查 /sys/kernel/debug/kref 或使用 trace_kref 跟踪 get/put 配对情况。

4.4 缓存对象泄漏(Slab 泄漏)

内核使用 slab 分配器(kmem_cache)管理频繁分配/释放的同类型对象。驱动通过 kmem_cache_alloc() 从缓存中获取对象,使用后应通过 kmem_cache_free() 归还。如果对象未归还,slab 缓存会持续膨胀,导致系统内存碎片化,甚至触发 OOM。

Slab 缓存对象泄漏示意 正常 Slab 缓存 对象1 对象2 对象3 空闲 对象分配后及时归还,缓存大小稳定 泄漏的 Slab 缓存 泄漏1 泄漏2 泄漏3 泄漏N 对象未归还,缓存持续增长,无法回收
// 缓存对象泄漏示例
static struct kmem_cache *my_cache;

struct my_obj {
    int id;
    char name[64];
};

static int __init my_init(void)
{
    my_cache = kmem_cache_create("my_obj_cache",
                                 sizeof(struct my_obj),
                                 0, 0, NULL);
    if (!my_cache)
        return -ENOMEM;
    return 0;
}

static struct my_obj *my_alloc(void)
{
    struct my_obj *obj;

    obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
    if (!obj)
        return NULL;

    // 使用 obj...
    // 错误:使用后未调用 kmem_cache_free(my_cache, obj);
    // 导致 slab 对象泄漏

    return obj;  // 调用者可能忘记释放
}

static void __exit my_exit(void)
{
    // 此时 my_cache 中仍有未归还的对象
    // kmem_cache_destroy 会报错并返回
    kmem_cache_destroy(my_cache);
}
module_exit(my_exit);
排查方法:
  • 查看 /proc/slabinfo,关注 my_obj_cache 的 active_objs 是否持续增长
  • 使用 slabtop 命令实时监控缓存大小变化
  • kmemleak 可以检测到 slab 缓存中未被释放的对象,但需要内核配置 CONFIG_DEBUG_KMEMLEAK

4.5 四种模式对比总结

泄漏模式 根本原因 检测难度 kmemleak 有效性 典型修复方法
分配后未释放 缺少释放调用 低 高(直接报告未引用对象) 补全 kfree/free_pages
错误路径未释放 条件分支遗漏 中 高(但需触发错误路径) goto 统一出口模式
引用计数泄漏 get/put 不配对 高 中(对象仍被引用,不显示为泄漏) 审计所有 get/put 路径
缓存对象泄漏 slab 对象未归还 中 高(配合 slab 调试) 确保 alloc/free 配对
重要提示: 在实际驱动开发中,这四种模式往往交织出现。例如,一个引用计数管理的对象可能同时包含 slab 缓存分配的内存,计数泄漏会导致 slab 对象也无法释放。kmemleak 是检测这些泄漏的利器,但理解泄漏模式能帮助开发者更快定位问题根源。