资讯动态

内核内存泄漏排查:slub debug 原理与实战指南

发布时间:2026/9/18 12:46:05 来源:尧图企业网站定制
做内核开发的人大概都有过这种经历系统跑着跑着内存越来越少业务进程没有明显泄漏dmesg里也看不到异常但/proc/meminfo里的 Slab 一栏就是只涨不跌。这种时候能快速揪出元凶的工具其实一直藏在内核里——slub debug。它不是第三方神器而是 SLUB 分配器自带的一套调试框架专门针对 kmalloc/kfree、kmem_cache_alloc/kmem_cache_free 这条链路上的内存问题。这篇文章我就以 linux kernel 内存泄漏检测为主线把 slub debug 的配置、原理、实战用法和踩坑记录完整梳理一遍适合正在被内核内存问题折磨的驱动开发者、内核新手和做稳定性保障的运维同学参考。1. 为什么说排查内核内存泄漏slub debug 是第一选择1.1 SLUB 分配器与它的调试基因提到内核内存分配器老读者应该记得历史上的三驾马车SLAB、SLOB、SLUB。SLUB 从 2.6.23 起成为默认分配器这一待就是十几年。SLUB 的设计哲学是“简洁 可扩展”它把原来 SLAB 里复杂的管理队列大幅简化同时还把调试能力直接做进了核心路径。只要内核编译时打开了CONFIG_SLUB_DEBUG你不需要额外加载任何内核模块就可以通过启动参数或 sysfs 接口对特定缓存做精细的监控。我经常把 slub debug 和另外两个工具放在一起对比kmemleak 和 KASAN。kmemleak 擅长扫描“已经没人引用但还占着内存”的孤儿内存定位方向是“谁分配了但没释放”KASAN 则是编译期插桩能抓越界读写、释放后使用等非常细的内存错误但开销大、需要重新编译内核。而 slub debug 的优势在于“可定点、可动态、开销可控”你可以在生产环境在特定缓存上临时打开它用最小代价复现问题。这也是我优先推荐它作为第一排查工具的原因。1.2 泄漏之外slub debug 能覆盖的问题清单很多文章一提到 slub debug 就只讲“查泄漏”实际上它是一套组合拳能处理的问题类型比想象的宽。从我的经验看它至少可以覆盖下面这些场景问题类型对应调试机制典型报错/表现内存泄漏分配未释放User tracking Tracealloc_calls 有记录free_calls 没有重复释放Sanity checks User trackingBUG kmalloc-XXX: Object already free使用已释放对象Poison User tracking访问 0x6b 填充区域或调用栈指向已 free 对象越界写/读Red Zone PoisonBUG kmalloc-XXX: Redzone overwritten未初始化使用Poison对象内容不是预期值触发校验失败freelist 被破坏Sanity checksFreepointer corrupt这里的关键思路是多个机制叠加使用能在一个报错里同时看到“谁分配的”“谁释放的”“哪个区域被踩了”等信息。后面我会逐个拆解这些机制的原理和实操方法。2. 环境准备先把 slub debug 开关打开2.1 内核配置项与编译要用 slub debug第一步是确认内核配置。最核心的选项是CONFIG_SLUB_DEBUG它决定了 SLUB 分配器是否包含调试代码。在这个基础上还有几个相关的开关值得注意CONFIG_SLUB_DEBUG_ON默认对所有 cache 打开 slub debug。CONFIG_SLUB_DEBUG_PANIC_ON检测到错误直接 panic适合在测试环境抓取完整 crash dump。CONFIG_SLAB_FREELIST_HARDENED对 freelist 指针做加密编码能防止部分类型的任意写攻击也会让部分损坏更早暴露。CONFIG_SLAB_FREELIST_RANDOM随机化 freelist 顺序配合 hardened 使用效果更好。CONFIG_SLAB_MERGE_DEFAULT允许相同大小的 kmalloc cache 合并注意合并后调试选项会互相影响。我建议调试内核直接开CONFIG_SLUB_DEBUGy和CONFIG_SLUB_DEBUG_ONy这样一启动所有 cache 就处于可调试状态省得后面后悔。另外要想让alloc_calls/free_calls里的调用栈可读还必须把CONFIG_KALLSYMS、CONFIG_STACKTRACE、CONFIG_DEBUG_INFO都打开否则你看到的是一串十六进制地址分析效率会低很多。2.2 启动参数与运行时动态开关编译完成后控制 slub debug 的主要手段是内核启动参数slub_debug。它的格式比较灵活最常用的几种写法我列一下# 全开所有调试选项 slub_debugFZPU # 只对指定 cache 开启 poison slub_debugP,kmalloc-512 # 对指定 cache 开启 user tracking slub_debugU,skbuff_head_cache # 排除某个 cache常用于关闭高频 cache 的调试 slub_debug-,kmalloc-64参数里的字母含义需要记一下F表示 Sanity checks一致性校验Z表示 Red zoning红区P表示 Poisoning毒化填充U表示 User tracking记录调用栈T表示 Trace分配/释放时打印调用栈A表示以上全部。格式上逗号前的部分是全局选项逗号后面跟一个或多个 cache 名称中间再用逗号隔开。运行时也有办法动态调整。以排查 kmalloc-256 为例# 打开特定 cache 的用户栈跟踪 echo 1 /sys/kernel/slab/kmalloc-256/trace echo 1 /sys/kernel/slab/kmalloc-256/store_user # 查看效果 cat /sys/kernel/slab/kmalloc-256/alloc_calls cat /sys/kernel/slab/kmalloc-256/free_calls需要说明的是运行时切换一般只影响之后新分配的对象已存在于 cache 中的对象可能还保留老状态所以“能提前开就提前开不能提前开就动态开再复现一轮”是基本操作。2.3 确认调试是否真的生效开关开了不代表一定能干活我遇到过几次开了slub_debug却完全没有调试信息的情况最后发现是编译配置或内核 cmdline 被 bootloader 覆盖了。所以验证“是否生效”这一步不能省。最简单的验证方法是看启动日志dmesg | grep -i slub如果你看到类似SLUB: HWalign64, Order0-3, MinObjects0, CPUs4, Nodes1的输出说明 SLUB 正常运行。要确认某个 cache 的调试选项是否真的打开可以读 sysfscat /sys/kernel/slab/kmalloc-256/poison cat /sys/kernel/slab/kmalloc-256/store_user cat /sys/kernel/slab/kmalloc-256/sanity_checks输出1表示对应功能已开启0则没开。如果配置都正确但还是拿不到调用栈优先检查/proc/sys/kernel/stack_trace_enabled是否被关闭以及内核是否编译了CONFIG_STACKTRACE。3. 深入了解 slub debug 的四大核心机制3.1 Poison用 0x5a 和 0x6b 标记对象状态Poisoning 是我用得最多的一个机制它原理不复杂分配对象时slub 会用特定字节模式填充整个对象释放对象时再用另一个字节模式重写一遍。内核里定义了两类经典毒化字节POISON_INUSE0x5a表示对象正在使用POISON_FREE0x6b表示对象已释放。这个机制的作用有三层。第一如果你在某个 cache 上开了 poison而代码里访问了已经释放的对象你会看到对象内容大量是 0x6b很容易在 hexdump 里发现端倪。第二分配时写下 0x5a 可以暴露“对象没初始化就使用”的问题因为正常驱动分配后会立刻覆盖这些字节。第三如果对象被释放后又被写入数据那 0x6b 模式会被破坏slub 在后续分配或 free 时校验会发现不一致并报错。实际使用中poison 对越界读的检测能力有限但它对“use-after-free”和“未初始化使用”有奇效而且开销比 KASAN 小得多。我通常在怀疑“释放后还在用”的问题时第一个就开P选项。3.2 Red Zone对象边界的哨兵Red Zone 的原理类似于内存保护边界slub 会在每个对象的起点和终点附近预留一小块特殊区域并写入固定的哨兵值。当代码越界读写跨过对象边界最先被破坏的往往是 red zoneslub 在分配、释放或一致性检查时就能发现。开启方式对应启动参数里的Z或 sysfs 里的red_zone。开 red zone 后每个对象实际占用的空间会变大因为要在前后额外附加保护区域并要求对象地址对齐到某个边界。所以你会发现开了 Z 后系统可用内存明显减少这在嵌入式环境里尤其明显。来看一个典型的报错长什么样BUG kmalloc-128 (Tainted: G O): Redzone overwritten ----------------------------------------------------------------------------- INFO: 0xffff888003350648-0xffff88800335064f offset1608. First byte 0x6b instead of 0xcc INFO: Allocated in alloc_netdev_mqs0x1a0/0x680 age100 cpu2 pid1234 INFO: Freed in free_netdev0x160/0x2c0 age68 cpu2 pid0看到First byte 0x6b instead of 0xcc这种信息时重点是它告诉了你“对象偏移多少位置被改写了”配合分配栈和释放栈基本能把问题定位到具体代码行。这也是我特别喜欢 red zone 的原因它能把“崩溃前”的异常行为变成“崩溃时”的明确报告。3.3 Store User 与 Trace把分配释放现场记下来如果说 poison 和 red zone 是“事后现场”那 Store User 和 Trace 就是“全程录像”。U选项会在对象头部记录最后一次分配和释放的内核调用栈读取/sys/kernel/slab/cache/alloc_calls和free_calls能看到每个调用点的统计。对于泄漏排查常用做法是先开U再观察cat /sys/kernel/slab/kmalloc-512/alloc_calls | head -40 cat /sys/kernel/slab/kmalloc-512/free_calls | head -40如果某个调用点在alloc_calls里频繁出现但在free_calls里几乎看不到对应的释放记录那八成就是泄漏源。T选项则更进一步它会在每次分配或释放时把调用栈直接打印到 dmesg适合在复现测试时逐条核对“分配了多少次、释放了多少次、配对关系是否正常”。这两个选项是最消耗性能的尤其是T可能让分配路径慢一个数量级所以我的习惯是先用/proc/slabinfo缩小范围到某个 cache再只对这个 cache 打开 U/T尽量避免全局开启。3.4 Sanity Checks主动一致性校验F选项对应一致性校验Sanity checks它会在对象分配、释放、cache 收缩等操作时检查对象状态、freelist 链表、对象标志等是否被破坏。如果说 red zone 是“被动等越界者踩线”那 sanity check 就是“主动巡逻”。从报错信息看sanity check 最常见的报错是Freepointer corrupt也就是 freelist 下一个节点的指针被改写了。这种情况通常意味着内存已经发生了比较严重的越界写或者任意写如果不及时处理后续 freelist 遍历会 crash 得莫名其妙。开启F后报错信息会指示是哪个 cache、哪个对象、freelist 指针从什么值变成了什么值配合 poison 可以比较快地判断是一般的越界还是块数据整体偏移。需要注意F的开销在核心分配路径上不小尤其在高并发场景下可能让分配热点明显变慢。我的建议是怀疑数据结构内部损坏时再开F不要默认全局开启。4. 实战演练一次 kmalloc 泄漏的完整排查过程4.1 现象收集与方向判断前阵子同事遇到一个诡异的稳定性问题一个虚拟网络设备模块反复执行 up/down 操作后系统可用内存持续下降最终触发 OOM。从free命令看Slab占了总内存一半以上。很多人看到 Slab 高第一反应就是泄漏但这里有个新手容易踩的坑SLUB 为了性能会缓存空闲对象即使没有泄漏Slab 数值也可能维持在一个较高水平。正确做法是观察趋势。我执行了下面这条命令watch -n 1 cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaim如果SUnreclaim持续上涨才更像真正的泄漏。然后通过/proc/slabinfo按活动对象数排序cat /proc/slabinfo | awk NR1 {print $2, $1} | sort -rn | head -20很快看到kmalloc-256的active_objs从几千涨到几万且没有回落趋势。方向基本锁定这个 cache 的分配与释放不配对。4.2 用 slub debug 锁定目标 cache锁定了 cache 后先看它属于哪个子系统。用slabinfo或直接看 cache 名称不够直观这时需要开 user tracking 来获取调用栈。在确认内核已开启CONFIG_SLUB_DEBUG后我这样操作echo 1 /sys/kernel/slab/kmalloc-256/store_user echo 1 /sys/kernel/slab/kmalloc-256/trace然后让同事复现一轮 up/down 操作。很快 dmesg 里出现了大量分配调用栈同时/sys/kernel/slab/kmalloc-256/alloc_calls中一个来自vnet_open函数的调用点出现频率暴涨而free_calls中对应vnet_close的频率却少得多。到这里问题范围已经缩得很小了每次 open 都有一次 kmalloc但并非每次 close 都有对应的 kfree。剩下的就是纯粹的代码审查。4.3 分析调用栈并修复翻代码后发现驱动在open里做了两件事分配一个私有结构体然后将其挂在netdev-ml_priv上。但如果设备已经被 open 过比如重复 open 的异常路径代码走了一个“提前返回”的分支导致旧指针被覆盖旧对象就泄漏了。更隐蔽的是这个分支只在某种控制命令下发时才会触发所以常规测试根本不会发现问题。修复方案很简单在重复 open 时先释放旧对象或者把ml_priv的赋值放到所有合法性检查之后。修复后再跑压力测试kmalloc-256的active_objs回落到稳定水平SUnreclaim也不再增长。这个案例里slub debug 的价值不只是“证明有泄漏”而是把调用点直接指到了函数级省去了大量盲查代码的时间。5. 常见问题与避坑指南5.1 性能开销太大怎么办这是开启 slub debug 后被问得最多的问题。开销主要来自三块每个对象要附加 red zone、freelist 编码、调用栈存储导致同样数量的对象占用更多内存cache 命中率下降。poison 需要在分配和释放时做整块内存填充对象越大开销越明显。trace 和 store_user 需要在分配/释放时记录调用栈栈回溯本身很耗时。我的建议是按“从轻到重”递进排查先用U定位分配点再用P检测释放后使用最后才上Z和F。生产环境尽量用针对单一 cache 的方式比如slub_debugU,kmalloc-256并限定在低峰窗口期复现避免全量开启导致业务抖动。5.2 日志刷屏与误报处理开启了T后dmesg 会被刷得飞快这时候你可能会漏掉真正有用的信息。我一般先关掉无关 cache 的 trace再用dmesg -w实时过滤关键词dmesg -w | grep -E BUG|INFO:|Allocated|Freed另外要小心一种“误报”某些模块会故意复用已释放的内存做性能优化比如提前把对象放进自己的 free list 延迟归还。这种场景下slub debug 的 poison 可能会在对象被重用前就被改写触发 “Object already free” 之类的报警。遇到这种情况先看调用栈里是不是有模块自己的 free 机制别急着改代码——有时候是“假阳性”有时候是模块自身设计存在隐患需要人工判断。5.3 与 kmemleak、KASAN 怎么配合从我实践来看这三种工具不是互斥关系而是互补关系。slub debug 适合“怀疑某个 cache 有问题”的阶段能快速缩小范围。kmemleak 适合“完全没有任何线索只知道内存少了”的情况它通过扫描内核内存来寻找孤立的已分配块输出报告格式是“unreferenced object ...”定位泄漏方向非常直接。KASAN 适合开发环境做深度检测它需要重新编译内核并在编译时对代码插桩能抓到 slub debug 抓不到的访问越界、全局缓冲区溢出等问题。我的建议是线上出问题时先用 slub debug 缩小范围代码改动和回归测试阶段如果时间允许开一套 KASAN 内核做全量验证。两个工具配起来基本能覆盖内核态内存问题的大多数场景。5.4 速查表常见 BUG 日志含义与排查方向报错关键字含义优先排查方向Redzone overwritten对象边界被越界写查Allocated in对应函数附近的 memcpy、copy_from_userObject already free对象被重复释放查释放路径是否有两条分支或释放回调是否被触发两次Freepointer corruptfreelist 指针被改写查对象内嵌结构体是否越界写配合 poison 看改写字节Object is not initialized使用了未初始化对象查 kmalloc 后的 memset/初始化逻辑或改用 kzallocalloc_calls异常且free_calls缺失对象分配后没释放查 open/close、probe/remove、create/destroy 的配对逻辑这个表不是标准文档而是我在实际排障中总结出来的“第一反应”。很多问题真正解决起来往往还需要结合具体模块的业务逻辑但方向对了效率会高很多。最后再说一个小技巧当你确定要修一个疑似泄漏点时可以用/sys/kernel/slab/cache/alloc_calls在修复前后各抓一次统计数据做对比不仅能看到总量下降还能确认修复是否影响了其他调用路径。别只盯着 dmesg 有没有报错数据对比才是“修没修干净”的最诚实答案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价