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、缓存和对象之间的关系:
排查直觉: 如果
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 综合排查流程
以下流程图总结了从发现异常到定位代码行的完整步骤:
注意事项:
- 不要在生产环境直接使用
CONFIG_DEBUG_SLAB,它会显著降低分配/释放性能。 - 如果缓存名是
kmalloc-*或dma-kmalloc-*,说明驱动使用了通用分配器而非专用缓存,此时需要结合kmemleak进一步定位。 - 对于
kmem_cache_create创建的专用缓存,缓存名就是驱动中传入的字符串,直接搜索源码即可找到创建点。
4.6 实战案例:USB 驱动 urb_context 泄漏
某 USB 网卡驱动在卸载后,slabtop 显示 usb_urb_context 缓存仍有 2000 个活动对象。通过以下步骤定位:
- 确认泄漏: 执行
slabtop -s c按缓存大小排序,发现usb_urb_context使用率 100%,且每次网络传输后 OBJS 增加 1。 - 关联驱动: 搜索内核源码中创建
usb_urb_context缓存的位置,定位到drivers/net/usb/usbnet.c。 - 代码审查: 发现
tx_complete回调中,当 URB 传输状态为-ENOENT(被取消)时,跳过了usb_free_urb和对应的kfree(context)。 - 修复: 在
tx_complete的-ENOENT分支中添加释放逻辑。 - 验证: 修复后,反复插拔和网络压力测试,
usb_urb_context对象数稳定在基线值。
核心原则: 每个
kmem_cache_alloc 都必须有对应的 kmem_cache_free,且必须在所有错误路径和正常路径中都得到执行。使用 slabtop + /proc/slabinfo 可以快速验证这一原则是否被违反。