4. 常见泄漏模式
内存泄漏并非单一现象,在内核驱动开发中,泄漏模式通常可以归纳为四种典型类型。理解这些模式是使用 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 跳转、异常提前返回)中遗漏了释放操作。常见于具有多层资源获取的初始化函数中。
// 错误示例:错误路径未释放
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。
// 缓存对象泄漏示例
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 是检测这些泄漏的利器,但理解泄漏模式能帮助开发者更快定位问题根源。