2. 内存泄漏基础:kmalloc/vmalloc与内存分配机制,泄漏的根源分析
内存泄漏是Linux设备驱动开发中最常见、最难排查的问题之一。要理解泄漏的根源,首先必须掌握内核态内存分配的两大核心机制:kmalloc 与 vmalloc。本章从分配器的底层原理出发,剖析泄漏产生的根本原因,并给出系统化的排查思路。
2.1 内核内存分配器全景
Linux内核为驱动开发者提供了多种内存分配接口,它们在物理连续性、分配速度、内存大小和虚拟映射方式上存在显著差异。下表对比了最常用的几种分配方式:
| 分配函数 | 物理连续性 | 虚拟连续性 | 最大分配量 | 分配速度 | 典型用途 |
|---|---|---|---|---|---|
kmalloc() |
连续 | 连续 | ~4 MB (受SLUB限制) | 快 | 小数据结构、DMA缓冲区 |
vmalloc() |
不连续 | 连续 | ~128 MB (可调) | 慢(需页表操作) | 大块缓冲区、模块加载 |
__get_free_pages() |
连续 | 连续 | 4 MB (order=10) | 快 | 页对齐的大块内存 |
dma_alloc_coherent() |
连续 | 连续 | 取决于平台 | 中等 | DMA一致性缓冲区 |
2.2 kmalloc 与 SLUB 分配器
kmalloc 底层依赖 SLUB 分配器(Linux 3.x 后的默认分配器)。SLUB 将内存划分为不同大小的 kmem_cache,每个 cache 管理固定大小的对象。当驱动调用 kmalloc(size, GFP_KERNEL) 时,SLUB 从最匹配的 cache 中取出一个对象返回。
以下是一个典型的泄漏代码模式:
/* 错误示例:每次中断处理都分配内存,但从未释放 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
struct my_data *data;
data = kmalloc(sizeof(*data), GFP_ATOMIC);
if (!data)
return IRQ_NONE;
/* 处理中断... 但忘记调用 kfree(data); */
return IRQ_HANDLED;
}
每次中断触发,都会从 SLUB cache 中取出一个对象,但从未归还。随着中断次数增加,该 cache 的 对象数量持续增长,最终耗尽整个 slab 页面。
2.3 vmalloc 与虚拟内存映射
vmalloc 分配的内存来自 vmalloc 区域(位于 vmalloc_start 到 vmalloc_end 之间)。分配时,内核在物理内存中寻找一组不连续的页框,然后在页表中建立连续的虚拟地址映射。释放时调用 vfree(),内核会解除映射并归还物理页。
vmalloc 的另一个常见陷阱是 部分释放:
/* 错误示例:只释放了部分映射 */
void *buf = vmalloc(4096 * 100); /* 分配 100 页 */
/* ... 使用 buf ... */
vfree(buf + 4096 * 50); /* 错误!只释放了后半部分 */
/* 前半部分 50 页永久泄漏 */
vmalloc 必须传入原始指针,不能偏移。内核没有提供“部分释放”的接口。
2.4 泄漏的根源分析:谁在“持有”内存?
内存泄漏的本质是:分配器认为内存仍被使用,但驱动已经丢失了指向该内存的所有引用。从内核视角看,泄漏分为两类:
- 显式泄漏:驱动调用了 kmalloc/vmalloc,但从未调用对应的 kfree/vfree。这是最常见的类型。
- 隐式泄漏:驱动将分配的内存指针存入某个链表、哈希表或全局变量,但在清理路径中未正确移除和释放。这类泄漏更难定位。
下图展示了 kmalloc 泄漏的完整生命周期:
2.5 泄漏的“冰山”效应
内存泄漏在驱动中往往表现为 缓慢累积。一次泄漏可能只有几十字节,但经过数小时甚至数天的运行,累积量会达到 MB 级别。以下是泄漏的典型症状:
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
/proc/meminfo 中 Slab 字段持续增长 |
kmalloc 泄漏 | 检查 /proc/slabinfo 中哪个 cache 增长最快 |
/proc/vmallocinfo 中条目数不断增加 |
vmalloc 泄漏 | 对比不同时间点的 vmalloc 区域使用量 |
| 系统运行一段时间后 oom_killer 被触发 | 严重内存泄漏 | 使用 kmemleak 扫描未释放对象 |
| 驱动卸载后内存占用未回落 | 模块退出路径未释放 | 检查 module_exit() 中的清理逻辑 |
2.6 排查泄漏的“三板斧”
当怀疑驱动存在内存泄漏时,按以下步骤系统化排查:
- 观察指标:使用
cat /proc/meminfo | grep -E "Slab|VmallocUsed"和cat /proc/slabinfo确认泄漏方向。 - 启用 kmemleak:内核配置
CONFIG_DEBUG_KMEMLEAK=y,挂载后执行echo scan > /sys/kernel/debug/kmemleak,然后cat /sys/kernel/debug/kmemleak查看未释放的分配栈。 - 代码审计:重点检查所有
kmalloc/vmalloc调用点,确保每个分配都有对应的kfree/vfree,且所有错误路径都包含释放逻辑。
kmemleak 进行回归测试。将 kmemleak 扫描集成到 CI 流程中,可以拦截 90% 以上的显式泄漏。