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 写上。就像关门时顺手摸一下钥匙——形成肌肉记忆。

核心原则: 分配和释放要成对出现。写代码时,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 个忘了释放。你说气不气人?

避坑指南: 我建议用 goto 统一出口。内核里大量使用这种模式,不是没有道理的。
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。

注意: 引用计数泄漏很难用 kmemleak 检测到。因为内存不是通过 kmalloc 分配的,而是从 slab 缓存里取的。我一般用 /proc/slabinfo 观察对象数量是否持续增长。

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 是否在退出时销毁?

知识体系总览

下面这张图,把这四种泄漏的关系和排查思路串起来了。你想想看,是不是这个理?

内存泄漏四大典型场景 内存泄漏 分配后未释放 kmalloc 后忘了 kfree 分支路径跳过释放 提前 return 漏了释放 引用计数泄漏 get/put 不平衡 内核对象未销毁 timer/workqueue/thread 残留 常用排查手段 kmemleak | /proc/slabinfo | 引用计数调试 | 静态分析 —— 对症下药,不同泄漏用不同工具 ——

这四种场景,你只要记住一个原则:谁分配,谁释放;谁引用,谁归还。 写代码时多问自己一句:“这个内存,到底谁负责释放?”

嗯,今天就先聊到这儿。下一节咱们深入看看 kmemleak 这个工具怎么用——它是我排查泄漏时的第一把刀。


专注资料整理