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一致性缓冲区
关键认知:kmalloc 分配的内存物理连续,适合 DMA 操作;vmalloc 分配的内存仅虚拟连续,适合大块但不需要 DMA 的场景。混淆两者的使用场景不会直接导致泄漏,但会引发性能问题或内存碎片。

2.2 kmalloc 与 SLUB 分配器

kmalloc 底层依赖 SLUB 分配器(Linux 3.x 后的默认分配器)。SLUB 将内存划分为不同大小的 kmem_cache,每个 cache 管理固定大小的对象。当驱动调用 kmalloc(size, GFP_KERNEL) 时,SLUB 从最匹配的 cache 中取出一个对象返回。

泄漏根源 #1:驱动调用了 kmalloc,但忘记调用 kfree。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(),内核会解除映射并归还物理页。

泄漏根源 #2:vmalloc 分配后未调用 vfree,不仅泄漏物理内存,还泄漏了宝贵的 vmalloc 虚拟地址空间。vmalloc 区域通常只有 128 MB~256 MB,泄漏速度比 kmalloc 更快。

vmalloc 的另一个常见陷阱是 部分释放:

/* 错误示例:只释放了部分映射 */
void *buf = vmalloc(4096 * 100);  /* 分配 100 页 */
/* ... 使用 buf ... */
vfree(buf + 4096 * 50);           /* 错误!只释放了后半部分 */
/* 前半部分 50 页永久泄漏 */

vmalloc 必须传入原始指针,不能偏移。内核没有提供“部分释放”的接口。

2.4 泄漏的根源分析:谁在“持有”内存?

内存泄漏的本质是:分配器认为内存仍被使用,但驱动已经丢失了指向该内存的所有引用。从内核视角看,泄漏分为两类:

  • 显式泄漏:驱动调用了 kmalloc/vmalloc,但从未调用对应的 kfree/vfree。这是最常见的类型。
  • 隐式泄漏:驱动将分配的内存指针存入某个链表、哈希表或全局变量,但在清理路径中未正确移除和释放。这类泄漏更难定位。

下图展示了 kmalloc 泄漏的完整生命周期:

kmalloc 内存泄漏生命周期 驱动调用 kmalloc() SLUB 分配对象 驱动使用对象 驱动返回/退出 忘记调用 kfree() 对象滞留 SLUB cache 指针丢失,无法释放 cache 对象计数持续增长

2.5 泄漏的“冰山”效应

内存泄漏在驱动中往往表现为 缓慢累积。一次泄漏可能只有几十字节,但经过数小时甚至数天的运行,累积量会达到 MB 级别。以下是泄漏的典型症状:

症状 可能原因 排查方向
/proc/meminfo 中 Slab 字段持续增长 kmalloc 泄漏 检查 /proc/slabinfo 中哪个 cache 增长最快
/proc/vmallocinfo 中条目数不断增加 vmalloc 泄漏 对比不同时间点的 vmalloc 区域使用量
系统运行一段时间后 oom_killer 被触发 严重内存泄漏 使用 kmemleak 扫描未释放对象
驱动卸载后内存占用未回落 模块退出路径未释放 检查 module_exit() 中的清理逻辑

2.6 排查泄漏的“三板斧”

当怀疑驱动存在内存泄漏时,按以下步骤系统化排查:

  1. 观察指标:使用 cat /proc/meminfo | grep -E "Slab|VmallocUsed" 和 cat /proc/slabinfo 确认泄漏方向。
  2. 启用 kmemleak:内核配置 CONFIG_DEBUG_KMEMLEAK=y,挂载后执行 echo scan > /sys/kernel/debug/kmemleak,然后 cat /sys/kernel/debug/kmemleak 查看未释放的分配栈。
  3. 代码审计:重点检查所有 kmalloc/vmalloc 调用点,确保每个分配都有对应的 kfree/vfree,且所有错误路径都包含释放逻辑。
最佳实践:在驱动开发阶段,始终使用 kmemleak 进行回归测试。将 kmemleak 扫描集成到 CI 流程中,可以拦截 90% 以上的显式泄漏。

2.7 本章小结