4、内存泄漏的典型场景:分配后未释放、错误的分支路径导致跳过释放、引用计数泄漏、内核对象未正确销毁
内存泄漏,说白了就是内存“借了不还”。
在内核里,这可不是小事。用户态程序泄漏了,大不了进程退出时系统帮你回收。内核泄漏?嗯,那就像你家水管一直在滴水——刚开始看不出来,等发现时墙都泡烂了。
我这些年排查过的内存泄漏问题,归纳起来其实就四类。咱们一个一个说。
4.1 分配后未释放
这是最“耿直”的泄漏。你分配了一块内存,用完了,但忘了释放。
看个例子:
void my_func(void)
{
struct my_data *data;
data = kmalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return;
// ... 用 data 做点事 ...
// 糟糕!忘了 kfree(data)
return;
}
这种问题,我早期刚写驱动时犯过不止一次。当时心里想:“不就少写一行吗?”结果系统跑了三天,kmalloc 开始返回 NULL 了。
我个人习惯是:每次写 kmalloc,立刻把对应的 kfree 写上。就像关门时顺手摸一下钥匙——形成肌肉记忆。
4.2 错误的分支路径导致跳过释放
这个比第一种更隐蔽。你明明写了释放代码,但某个分支提前 return 了,释放代码根本没执行到。
void process_data(struct device *dev)
{
struct my_data *data;
int ret;
data = kmalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return;
ret = some_operation(dev);
if (ret < 0) {
// 这里直接 return 了!
// 忘了 kfree(data)
return ret;
}
// 正常路径
kfree(data);
return 0;
}
为什么会这样?因为写代码时,我们往往先写正常路径,再补错误处理。补着补着就漏了。
我曾经在调试一个网络驱动时,花了整整两天才找到这种泄漏。那个函数有 7 个错误返回点,其中 3 个忘了释放。你说气不气人?
void process_data(struct device *dev)
{
struct my_data *data;
int ret = 0;
data = kmalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
ret = some_operation(dev);
if (ret < 0)
goto out_free;
// ... 其他操作 ...
out_free:
kfree(data);
return ret;
}
你看,一个出口,所有释放都在那里。不管中间有多少分支,最后都会走到这里。干净利落。
4.3 引用计数泄漏
引用计数泄漏,是内核里最“阴”的泄漏。为什么?因为你看不到 kmalloc,也就想不到 kfree。
内核里很多对象是靠引用计数管理的。比如 struct file、struct inode、struct module。你拿了一个引用,用完了就得放。拿一次,放一次,计数归零时自动销毁。
void use_file(struct file *filp)
{
// 获取引用
get_file(filp);
// 用一下
do_something(filp);
// 忘了 fput()!引用计数永远降不到 0
// 这个 file 结构体就泄漏了
}
我在项目中遇到过最离谱的一次:一个内核模块卸载后,slab 内存池里还有几百个“已死”的 struct file。原因就是某个路径下 fput 被跳过了。
引用计数泄漏的可怕之处在于:它不会立刻导致问题。你 insmod、rmmod 十次八次都没事。但次数多了,系统就慢慢“变胖”,直到 OOM。
4.4 内核对象未正确销毁
最后一种,是内核对象的生命周期管理出了问题。
比如你创建了一个内核线程、一个 timer、一个 workqueue,或者注册了一个 notifier。这些“对象”在模块卸载或设备移除时,必须被正确销毁。
static struct timer_list my_timer;
void init_module(void)
{
setup_timer(&my_timer, my_timer_callback, 0);
mod_timer(&my_timer, jiffies + HZ);
}
void cleanup_module(void)
{
// 忘了 del_timer()!
// 模块卸载了,但 timer 还在跑
// 一旦触发,回调函数访问的是已释放的内存
}
这种泄漏,往往伴随着更严重的问题——内存被释放后还在用,也就是 use-after-free。内核 panic 是常有的事。
我个人的经验是:创建和销毁要写在同一个函数里,或者至少写在同一个文件中,上下能对照。如果创建在 init,销毁在 exit,那就要反复检查,确保一一对应。
- kmalloc / kfree 是否成对?
- 每个错误路径是否都释放了?
- get / put 引用计数是否平衡?
- timer、workqueue、thread 是否在退出时销毁?
知识体系总览
下面这张图,把这四种泄漏的关系和排查思路串起来了。你想想看,是不是这个理?
这四种场景,你只要记住一个原则:谁分配,谁释放;谁引用,谁归还。 写代码时多问自己一句:“这个内存,到底谁负责释放?”
嗯,今天就先聊到这儿。下一节咱们深入看看 kmemleak 这个工具怎么用——它是我排查泄漏时的第一把刀。