4. 内存泄漏检测工具:使用 slabtop 与 /proc/slabinfo 分析内核对象泄漏

内核对象泄漏是设备驱动开发中最隐蔽的内存问题之一。与用户空间不同,内核无法依赖进程退出自动回收资源,一次未释放的 kmem_cache_alloc 或 kzalloc 会永久占用 slab 内存,直至系统重启。本节将围绕 slabtop 和 /proc/slabinfo 两个核心工具,建立一套从“发现异常”到“定位泄漏对象”的排查流程。

关键概念: Slab 分配器是 Linux 内核管理小对象(通常小于 128KB)的内存机制。每个“缓存”(cache)对应一类内核对象(如 task_struct、inode、驱动私有结构体)。slabtop 提供实时视图,/proc/slabinfo 提供可脚本化的原始数据。

4.1 理解 slab 内存布局与泄漏特征

在动手排查之前,需要先理解 slab 分配器的基本结构。下图展示了 slab、缓存和对象之间的关系:

Slab 分配器结构:缓存 → slab → 对象 cache_chain kmalloc-128 my_drv_obj slab 0 slab 1 slab 0 slab 0 … obj obj 泄漏 obj obj obj obj obj obj 泄漏特征:某个缓存的对象总数(active_objs)持续增长,而 slab 数量(num_slabs)也随之增加,系统无法回收。
排查直觉: 如果 my_drv_obj 缓存中的对象数在驱动加载后只增不减,且每次 I/O 操作后数量上升,几乎可以断定存在对象泄漏。

4.2 使用 slabtop 进行实时监控

slabtop 是 procps-ng 包提供的交互式工具,默认按对象数量排序,实时刷新。以下是典型输出片段及关键字段解读:

# slabtop -o  # -o 表示只显示活跃缓存(active caches)

 Active / Total Objects (% used) : 183204 / 189012 (96.9%)
 Active / Total Slabs (% used)   : 10234 / 10500 (97.5%)
 Active / Total Caches (% used)  : 98 / 128 (76.6%)

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
 45000 44980   99%    0.10K   1125      40       4500K my_drv_obj
 32000 31950   99%    0.25K   2000      16       8000K kmalloc-256
 21000 20800   99%    0.50K   2625       8      10500K dentry
 18000 17980   99%    0.19K   1800      10       3600K inode_cache
字段 含义 泄漏指示
OBJS 该缓存当前分配的对象总数(包括已用和空闲) 持续增长 → 泄漏
ACTIVE 当前正在使用的对象数 接近 OBJS 且不下降 → 泄漏
USE % 使用率 = ACTIVE / OBJS 持续 99%~100% → 泄漏
SLABS 分配给该缓存的 slab 数量 随 OBJS 同步增长 → 泄漏
CACHE SIZE 该缓存占用的总内存 持续上升 → 泄漏
常见陷阱: 不要只看 OBJS 的绝对值。某些缓存(如 dentry、inode)在正常系统中也会保持大量对象。泄漏的关键特征是 持续增长且不回落,尤其是在执行特定操作(如打开/关闭设备、提交 URBs)前后对比。

4.3 使用 /proc/slabinfo 进行精确采样

/proc/slabinfo 提供与 slabtop 相同的数据,但格式更适合脚本化采集。通过两次采样之间的差值,可以精确计算泄漏速率。

# 第一次采样
cat /proc/slabinfo | grep my_drv_obj > /tmp/slab_before.txt

# 执行 1000 次设备打开/关闭操作
for i in $(seq 1 1000); do echo "open/close test $i"; done

# 第二次采样
cat /proc/slabinfo | grep my_drv_obj > /tmp/slab_after.txt

# 对比
diff /tmp/slab_before.txt /tmp/slab_after.txt

/proc/slabinfo 每一行的格式如下:

# 字段顺序(从 slabinfo 5.0+ 开始):
# cache_name       : 
# active_objs      : 当前活动对象数
# num_objs         : 总对象数
# objsize          : 每个对象大小(字节)
# objperslab       : 每个 slab 包含的对象数
# pagesperslab     : 每个 slab 占用的页数
# limit            : 最大 slab 数(0 表示无限制)
# batchcount       : 批量分配/释放数
# sharedfactor     : 共享因子
# slab_data        : 内部标志

# 示例行:
my_drv_obj         45000  45000  100  40  1 : tunables    0    0    0 : slabdata 1125 1125 0
脚本化泄漏检测模板:
#!/bin/bash
# 每 5 秒采样一次 my_drv_obj 的活动对象数
while true; do
  echo "$(date +%T) $(awk '/my_drv_obj/ {print $2}' /proc/slabinfo)"
  sleep 5
done

如果输出序列为 45000 → 45050 → 45100 → ...,说明每 5 秒泄漏约 50 个对象。

4.4 定位泄漏源:从缓存名到驱动代码

一旦确认某个缓存存在泄漏,下一步是找到创建该缓存的代码位置。内核中创建 slab 缓存的典型方式如下:

#include <linux/slab.h>

static struct kmem_cache *my_cache;

static int __init my_driver_init(void)
{
    // 创建缓存,名称会出现在 /proc/slabinfo 中
    my_cache = kmem_cache_create("my_drv_obj",
                                 sizeof(struct my_private),
                                 0, 0, NULL);
    if (!my_cache)
        return -ENOMEM;
    return 0;
}

static void __exit my_driver_exit(void)
{
    kmem_cache_destroy(my_cache);
}

排查泄漏时,重点检查以下三种模式:

  • 分配后未释放: 使用 kmem_cache_alloc(my_cache, GFP_KERNEL) 后,对应的 kmem_cache_free(my_cache, ptr) 在错误路径中被跳过。
  • 引用计数泄漏: 对象内嵌 kref 或 atomic_t,释放函数依赖引用计数归零,但某条路径多了一次 kref_get。
  • 容器结构未释放: 驱动分配了一个包含多个子对象的容器,只释放了子对象而忘记释放容器本身。
进阶技巧: 打开内核的 CONFIG_DEBUG_SLAB 或 CONFIG_SLUB_DEBUG,可以在 /sys/kernel/slab/<cache_name>/ 下获得更详细的分配栈回溯。配合 trace-cmd 或 ftrace 的 kmem:kmem_cache_alloc 事件,可以捕获每次分配的调用栈。

4.5 综合排查流程

以下流程图总结了从发现异常到定位代码行的完整步骤:

内核对象泄漏排查流程 1. slabtop 发现异常缓存 2. /proc/slabinfo 确认增长趋势 3. 通过缓存名找到驱动模块 4a. 代码审查:kmem_cache_alloc 4b. 启用 SLUB_DEBUG + ftrace 5. 定位未释放的路径并修复 6. 重新测试,确认对象数不再增长
注意事项:
  • 不要在生产环境直接使用 CONFIG_DEBUG_SLAB,它会显著降低分配/释放性能。
  • 如果缓存名是 kmalloc-* 或 dma-kmalloc-*,说明驱动使用了通用分配器而非专用缓存,此时需要结合 kmemleak 进一步定位。
  • 对于 kmem_cache_create 创建的专用缓存,缓存名就是驱动中传入的字符串,直接搜索源码即可找到创建点。

4.6 实战案例:USB 驱动 urb_context 泄漏

某 USB 网卡驱动在卸载后,slabtop 显示 usb_urb_context 缓存仍有 2000 个活动对象。通过以下步骤定位:

  1. 确认泄漏: 执行 slabtop -s c 按缓存大小排序,发现 usb_urb_context 使用率 100%,且每次网络传输后 OBJS 增加 1。
  2. 关联驱动: 搜索内核源码中创建 usb_urb_context 缓存的位置,定位到 drivers/net/usb/usbnet.c。
  3. 代码审查: 发现 tx_complete 回调中,当 URB 传输状态为 -ENOENT(被取消)时,跳过了 usb_free_urb 和对应的 kfree(context)。
  4. 修复: 在 tx_complete 的 -ENOENT 分支中添加释放逻辑。
  5. 验证: 修复后,反复插拔和网络压力测试,usb_urb_context 对象数稳定在基线值。
核心原则: 每个 kmem_cache_alloc 都必须有对应的 kmem_cache_free,且必须在所有错误路径和正常路径中都得到执行。使用 slabtop + /proc/slabinfo 可以快速验证这一原则是否被违反。