1. 内存泄漏基础
1.1 什么是内存泄漏
内存泄漏(Memory Leak)是指程序在运行过程中动态分配的堆内存,由于程序设计错误导致在不再需要时无法被释放,从而造成系统可用内存持续减少的现象。在 Linux 内核开发中,内存泄漏通常表现为通过 kmalloc()、vmalloc()、alloc_pages() 等接口分配的内存块,其指针丢失或引用计数失衡,导致内存无法归还给系统。
核心定义: 内存泄漏 = 已分配堆内存 + 丢失释放路径 + 持续累积
以下是一个典型的内核内存泄漏代码示例:
#include <linux/slab.h>
#include <linux/module.h>
static int __init leak_init(void)
{
char *buf;
int i;
for (i = 0; i < 100; i++) {
buf = kmalloc(1024, GFP_KERNEL); /* 每次分配 1KB */
if (!buf)
return -ENOMEM;
/* 忘记调用 kfree(buf) —— 内存泄漏 */
}
return 0;
}
module_init(leak_init);
上述代码每次调用 kmalloc() 后,指针 buf 在下一轮循环中被覆盖,之前分配的内存块失去所有引用,永远无法释放。
1.2 内存泄漏的危害
内存泄漏在 Linux 内核中属于严重缺陷,其危害随泄漏速率和系统运行时间呈指数级增长。以下是主要危害维度:
| 危害类型 | 具体表现 | 严重等级 |
|---|---|---|
| 系统性能下降 | 可用内存减少,触发频繁的页面回收和交换,系统响应变慢 | 中 |
| 内存耗尽(OOM) | 系统无法分配新内存,触发 OOM Killer 杀死进程,甚至内核 panic | 高 |
| 系统不稳定 | 关键内核模块因内存不足而异常,导致随机崩溃或死锁 | 高 |
| 资源浪费 | 泄漏的内存无法被其他进程或内核组件复用,降低硬件利用率 | 中 |
| 调试困难 | 泄漏通常需要长时间运行才能暴露,定位根因耗时巨大 | 极高 |
生产环境案例: 某网络设备驱动在热拔插场景下每次泄漏 4KB,以每分钟 100 次操作计算,24 小时后泄漏约 562MB,直接导致系统 OOM 重启。
1.3 常见内存泄漏场景概述
内核开发中,内存泄漏的根因通常集中在以下几个典型场景。理解这些场景有助于在代码审查和调试时快速定位问题。
1.3.1 分配后未释放
最直接的泄漏形式:调用 kmalloc()、kzalloc()、vmalloc() 等分配函数后,在函数返回或错误处理路径中遗漏对应的 kfree()、vfree() 调用。
1.3.2 引用计数失衡
内核对象(如 struct kobject、文件、网络套接字)通过引用计数管理生命周期。当 kobject_get() 与 kobject_put() 不匹配时,对象无法被释放,形成泄漏。
1.3.3 链表/哈希表节点未清理
将动态分配的内存节点插入链表或哈希表后,在删除节点时只执行了 list_del() 而未释放节点内存,导致节点成为泄漏点。
1.3.4 错误处理路径遗漏
在复杂的错误处理逻辑中,部分分支遗漏了已分配资源的释放。这是内核模块中最常见的泄漏来源。
1.3.5 缓存/池化对象泄漏
使用内存池(mempool)或 slab 缓存时,对象借出后未归还,或缓存清理函数未正确释放所有对象。
场景对比速查表
| 场景 | 典型 API | 检测难度 | 常见于 |
|---|---|---|---|
| 分配未释放 | kmalloc / kfree |
低 | 驱动 probe/remove 函数 |
| 引用计数失衡 | kobject_get / kobject_put |
中 | 设备模型、文件系统 |
| 链表节点泄漏 | list_add / list_del |
中 | 网络协议栈、缓存管理 |
| 错误路径遗漏 | 多分支 goto 清理 |
高 | 复杂初始化函数 |
| 缓存/池泄漏 | mempool_alloc / mempool_free |
高 | 块设备层、DMA 缓冲区 |