3. 内存泄漏的典型模式:未释放的 kmalloc、slab 对象泄漏、引用计数泄漏、页面泄漏

内核内存泄漏并非单一故障,而是由多种编程疏忽导致的资源未归还。理解泄漏的典型模式,是编写有效 SystemTap 探测脚本的前提。本章将剖析四种最常见的内核内存泄漏模式,并给出每种模式在 SystemTap 下的可观测特征。

3.1 未释放的 kmalloc 堆内存

这是最直观的泄漏模式:驱动或内核模块通过 kmalloc()、kzalloc() 或 vmalloc() 分配了内存,但在错误路径或正常释放点遗漏了对应的 kfree() 或 vfree()。

关键特征: 分配次数与释放次数不匹配。每次分配都会增加进程的 RSS 或系统 Slab 使用量,但释放从未发生。

SystemTap 可以通过跟踪 kmalloc 和 kfree 的调用点,统计每个调用栈的净分配量。以下是一个简单的计数脚本片段:

global alloc_count, free_count

probe kernel.trace("kmalloc") {
    alloc_count[backtrace()] <<< 1
}

probe kernel.trace("kfree") {
    free_count[backtrace()] <<< 1
}

probe timer.s(60) {
    foreach (bt in alloc_count) {
        net = @count(alloc_count[bt]) - @count(free_count[bt])
        if (net > 100)
            printf("泄漏栈: %s, 净分配: %d\n", sprint_ustack(bt), net)
    }
}
调试提示: 使用 backtrace() 而非 execname() 定位泄漏点。同一个模块的不同函数可能都调用 kmalloc,只有调用栈才能精确区分哪个路径遗漏了 kfree。
kmalloc 泄漏生命周期 kmalloc(size, GFP) kfree(ptr) 内存归还 kmalloc(size, GFP) 错误路径返回 内存泄漏(无 kfree) 正常释放路径

3.2 Slab 对象泄漏

Slab 分配器管理固定大小的对象缓存(如 kmem_cache_alloc)。泄漏通常发生在对象被分配后,使用者忘记调用 kmem_cache_free 将其归还给缓存。这种泄漏比普通 kmalloc 更难发现,因为 Slab 缓存可能长期保持“部分满”状态,不会立即触发 OOM。

指标 正常状态 泄漏状态
/proc/slabinfo 中对象总数 稳定或缓慢增长 持续线性增长
活跃对象数 与使用量匹配 远高于预期
缓存大小 稳定 持续增长

SystemTap 可以挂钩 kmem_cache_alloc 和 kmem_cache_free 的 tracepoint,按缓存名称统计净分配量:

global slab_alloc, slab_free

probe kernel.trace("kmem_cache_alloc") {
    slab_alloc[$cache_name, backtrace()] <<< 1
}

probe kernel.trace("kmem_cache_free") {
    slab_free[$cache_name, backtrace()] <<< 1
}

probe timer.s(120) {
    foreach ([name, bt] in slab_alloc) {
        net = @count(slab_alloc[name, bt]) - @count(slab_free[name, bt])
        if (net > 50)
            printf("Slab %s 泄漏 %d 对象, 栈: %s\n", kernel_string(name), net, sprint_backtrace(bt))
    }
}
常见陷阱: 某些 Slab 缓存(如 dentry、inode_cache)在 VFS 压力下会自然增长。不要仅凭对象总数判断泄漏,必须结合调用栈确认是否有未释放的分配路径。
Slab 对象泄漏检测流程 kmem_cache_alloc 记录调用栈 + 缓存名 kmem_cache_free 匹配释放记录 未匹配 = 泄漏对象 输出泄漏调用栈

3.3 引用计数泄漏

内核对象(如 struct file、struct inode、struct sk_buff)通过引用计数管理生命周期。每次 get() 或 kref_get() 增加计数,put() 或 kref_put() 减少计数。当计数无法归零时,对象及其关联内存永远不会被释放。

引用计数泄漏的典型症状:

  • 文件描述符泄漏: /proc/<pid>/fd 数量持续增长,但应用并未打开新文件。
  • 网络套接字泄漏: ss -s 显示大量 TIME_WAIT 或 CLOSE_WAIT 状态的 socket。
  • 设备引用泄漏: 卸载模块时提示 Device or resource busy,即使没有进程在使用。

SystemTap 可以通过跟踪 atomic_inc 和 atomic_dec 来监控特定对象的引用计数变化:

global ref_tracker

probe kernel.function("get_file") {
    ref_tracker[tid(), $file] <<< 1
}

probe kernel.function("fput") {
    ref_tracker[tid(), $file] <<< -1
}

probe timer.s(30) {
    foreach ([tid, file] in ref_tracker) {
        net = @sum(ref_tracker[tid, file])
        if (net > 5)
            printf("PID %d 文件 %p 引用计数净增 %d\n", tid, file, net)
    }
}
高级技巧: 对于 kref 管理的对象,可以挂钩 kref_get 和 kref_put 的返回值。当 kref_put 返回 1 时表示对象被释放,如果从未返回 1 则说明泄漏。
引用计数泄漏示意图 正常对象 refcount=1 get/put 平衡 释放 泄漏对象 refcount=3 put 缺失 2 次 对象永远驻留

3.4 页面泄漏

页面泄漏发生在物理页面分配器层面。驱动通过 alloc_pages() 或 __get_free_pages() 获取连续物理页,但未调用 __free_pages() 归还。这种泄漏会直接减少 MemFree 和 MemAvailable,严重时导致系统无法分配连续大块内存。

页面泄漏的检测指标:

指标来源 正常值 泄漏警告值
/proc/meminfo 的 MemFree 稳定或缓慢波动 持续下降,重启后恢复
/proc/buddyinfo 的连续块数 各 order 块数稳定 高阶块(order ≥ 3)持续减少
vmstat 的 pgalloc / pgfree 差值接近 0 差值持续为正且增长

SystemTap 可以通过跟踪 __alloc_pages_nodemask 和 free_pages 来监控页面分配与释放的平衡:

global page_alloc, page_free

probe kernel.function("__alloc_pages_nodemask").return {
    if ($return != 0)
        page_alloc[backtrace()] <<< 1
}

probe kernel.function("free_pages") {
    page_free[backtrace()] <<< 1
}

probe timer.s(60) {
    foreach (bt in page_alloc) {
        net = @count(page_alloc[bt]) - @count(page_free[bt])
        if (net > 10)
            printf("页面泄漏 %d 页, 栈: %s\n", net, sprint_backtrace(bt))
    }
}
性能注意: __alloc_pages_nodemask 是热路径函数,在生产环境高频挂钩可能导致显著延迟。建议使用 kernel.trace("mm_page_alloc") 和 kernel.trace("mm_page_free") tracepoint 替代,开销更低。
页面泄漏对伙伴系统的影响 正常伙伴系统 order-0: 128 块 order-3: 16 块 order-6: 4 块 泄漏后伙伴系统 order-0: 64 块 order-3: 4 块 order-6: 0 块 高阶块被泄漏页面碎片化,无法合并回大块 最终:分配大块内存失败,触发 OOM

3.5 四种泄漏模式的对比与选择

在实际工程中,一次内存泄漏可能同时涉及多种模式。下表帮助你在 SystemTap 脚本设计时快速定位应优先跟踪的指标:

泄漏模式 分配函数 释放函数 SystemTap 首选探测点 典型泄漏速率
kmalloc 泄漏 kmalloc kfree kernel.trace("kmalloc") 快(每次调用泄漏)
Slab 对象泄漏 kmem_cache_alloc kmem_cache_free kernel.trace("kmem_cache_alloc") 中(取决于对象使用频率)
引用计数泄漏 kref_get / get_file kref_put / fput kernel.function("kref_get") 慢(多次 get 才泄漏一个对象)
页面泄漏 alloc_pages free_pages kernel.trace("mm_page_alloc") 极快(每页 4KB,大量泄漏)
实战建议: 当系统出现 slabinfo 中某个缓存持续增长时,优先怀疑 Slab 对象泄漏;当 MemFree 直线下降但 Slab 无明显变化时,优先检查页面泄漏;当文件描述符或 socket 数量异常时,优先检查引用计数泄漏。