使用 kmemleak
回想一下我们在上一节结尾看到的场景:成百上千个 vm_area_struct 对象被分配出来,堆在内存里,没人去管。当时我们说,这是一种合理的系统行为——直到它不再合理为止。
问题是,你怎么知道什么时候是「合理」的,什么时候是「泄漏」的开始?
当你在写一个内核模块,或者调试某个驱动时,内存这种东西就像是指缝里的沙子——你以为你握紧了,其实它一直在漏。我们需要一把能够照出这些「孤儿」内存的探照灯。
这就是 kmemleak 存在的意义。
它的名字听起来像是个黑客工具,其实它的工作原理非常朴素,甚至有点「笨」:它像一个不知疲倦的清洁工,定期扫描整个内核内存,寻找那些已经被分配出来,但没有任何指针指向它们的内存块。如果发现了一块内存既没被释放,也没有任何指针指着它,kmemleak 就会判定:这是一个泄漏。
这一节,我们要做的就是这个——把 kmemleak 打开,让它跑起来,然后把我们的那些烂代码扔进去,看看它是怎么把这些尴尬的秘密揭露出来的。
6.1 基础流程——五步法
使用 kmemleak 的基本流程其实非常机械,甚至有点枯燥。我们可以把它浓缩成一个标准的五步 checklist。但在你跟着做之前,有一个重要的前提需要确认。
第 0 步:确认环境(至关重要)
在开始之前,你得确保两件事:
- debugfs 挂没挂?
通常它都在
/sys/kernel/debug下。如果这目录不存在,你得先动手把它挂上。 - kmemleak 到底启动了没?
这是个大坑。你以为内核配置里选了它就能用?未必。它可能处于「已配置但已禁用」的状态。如果
echo scan命令报错,十有八九就是这个问题。
假设上面这两点都搞定了,接下来的五步就是标准的操作流:
第 1 步:让它脏起来
运行你那个可能漏洞百出的代码,或者把测试用例跑起来。这一步的目的是「制造现场」——让内存泄漏发生。
第 2 步:扫描(这也是最核心的一步)
现在,手动触发一次内存扫描。以 root 身份执行:
echo scan > /sys/kernel/debug/kmemleak
这行命令会唤醒一个名为 kmemleak 的内核线程。它不是真的用眼睛看,而是遍历内存,检查指针的引用关系。
这个过程可能会有点慢。在我的虚拟机上,这通常需要几秒钟。如果你的机器内存很大,或者负载很高,你可能得稍微等一等。
如果它找到了怀疑对象,内核日志里会弹出一行这样的消息:
kmemleak: 1 new suspected memory leaks (see /sys/kernel/debug/kmemleak)
第 3 步:查看报告
就像日志里提示的那样,去读取那个伪文件:
cat /sys/kernel/debug/kmemleak
第 4 步:打扫战场(可选)
如果你想进行下一轮测试,不想被上一轮的报告干扰,清空它:
echo clear > /sys/kernel/debug/kmemleak
6.2 当你无法写入 kmemleak 时——排错实战
听上去很简单,对吧?
但在现实中,你在第 2 步就会撞墙。当你满怀信心地敲下 echo scan 时,终端可能冷冷地回你一句:
# echo scan > /sys/kernel/debug/kmemleak
bash: echo: write error: Operation not permitted
这时候你的第一反应可能是:「我不是用 root 跑的吗?」
别急,这不是权限问题。去内核日志里找找线索(用 dmesg 或 journalctl -k),你很可能会看到这行刺眼的红色字:
kmemleak: Kernel memory leak detector disabled
这就很奇怪了——明明配置里开启了 CONFIG_DEBUG_KMEMLEAK,为什么它还是说自己被禁用了?
这里有一个非常有意思的调试技巧,我们在前面章节提到过。我们可以用 debug 和 initcall_debug 内核参数来重新启动,看看启动过程中到底发生了什么。
给内核启动参数加上这两个选项:
debug initcall_debug
重启进系统后,查看一下启动日志,专门搜索 kmemleak 相关的消息:
[ ... ] kmemleak: kmemleak_late_init() failed (-12)
看到了吗?返回值是 -12。
玩转内核错误码的直觉告诉你:-12 对应的是 ENOMEM(内存不足)。
停一下。这很反直觉。
kmemleak 是一个内存泄漏检测工具,它自己竟然因为内存不足而初始化失败了?这就像是一个修水管的人因为水管没水而渴死了一样。
其实,kmemleak 在启动早期需要一个小的内存池(mem pool)来记录日志,这个池子的大小是由内核配置项 CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE 决定的,默认只有 16000 字节。在某些情况下,这点小钱确实不够花。
于是,你像模像样地拿出 scripts/config 工具,把这个值翻了一倍:
$ scripts/config --set-val CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE 32000
重新编译内核,重启。
结果……
$ dmesg | grep "kmemleak"
kmemleak: Kernel memory leak detector disabled
还是这个鬼样子。
这时候如果你把头埋进日志里研究半天内存池的大小,你就掉进了陷阱(所谓的 red herring)。问题根本不在这里。
问题的真正原因非常简单,简单到你甚至会觉得有点「蠢」。
你还需要再做一个动作:在内核启动命令行里明确地告诉它「请开启」。
默认情况下,即使你编译了 kmemleak,它也是默认关闭的(因为有性能开销)。你必须在启动参数里加上:
kmemleak=on
这才是那把真正的钥匙。
加上这个参数重启,你再看看日志:
$ dmesg | grep "kmemleak"
[ 6.743927] kmemleak: Kernel memory leak detector initialized (mem pool available: 14090)
[ 6.743956] kmemleak: Automatic memory scanning thread started
这就对了。那个内存池的大小警告甚至都不见了(因为现在它成功初始化了)。
⚠️ 踩坑预警 这个坑简直经典。 要点:配置
CONFIG_DEBUG_KMEMLEAK只是造好了车,想开走还得插钥匙——那就是kmemleak=on。 副作用:忘了这个参数,你会对着Operation not permitted抓狂半天,最后发现只是缺个启动参数。
顺便一提,那个被唤醒的 kmemleak 内核线程,它的优先级是被故意调低的(nice 值为 10),这意味着它只在系统比较闲的时候才会出来干活,不会抢着你正常业务的资源。
6.3 实战演练——捕捉泄漏
好了,工具修好了。现在是时候找几个真正的「犯人」来练练手了。
我们准备了三个测试用例。别眨眼,我们要开始抓虫了。
测试用例 3.1:教科书式的泄漏
先看第一段代码。这段代码烂得很直接:
// ch5/kmembugs_test/kmembugs_test.c
void leak_simple1(void)
{
volatile char *p = NULL;
pr_info("testcase 3.1: simple memory leak testcase 1\n");
p = kzalloc(1520, GFP_KERNEL);
if (unlikely(!p))
return;
pr_info("kzalloc(1520) = 0x%px\n", p);
if (0) // test: ensure it isn't freed
kfree((char *)p);
#ifndef CONFIG_MODULES
pr_info("kmem_cache_alloc(task_struct) = 0x%px\n",
kmem_cache_alloc(task_struct, GFP_KERNEL));
#endif
pr_info("vmalloc(5*1024) = 0x%px\n", vmalloc(5*1024));
}
这段代码里有三处泄漏:
kzalloc分配了 1520 字节。kmem_cache_alloc分配了一个task_struct(如果CONFIG_MODULES没定义的话)。vmalloc分配了 5KB。
最讽刺的是那个 if (0),这就像是明目张胆地写着:「我知道该释放,但我就是不想。」
把它跑起来:
cd <booksrc>/ch5/kmembugs_test
sudo ./run_tests
...
(Type in the testcase number to run): 3.1
Running testcase "3.1" via test module now...
[ ... ] kzalloc(1520) = 0xffff888003f17000
[ ... ] vmalloc(5*1024) = 0xffffc9000005c000
代码跑完了,内存也漏了。现在,让 kmemleak 出场:
sudo sh -c "echo scan > /sys/kernel/debug/kmemleak"
等它跑完(那几秒钟的停顿感有点微妙),看看报告:
sudo cat /sys/kernel/debug/kmemleak
你会看到一堆输出。别慌,我们只看第一个泄漏报告:
unreferenced object 0xffff8880127f8000 (size 2048):
comm "run_tests", pid 5498, jiffies 4296684850 (age 84.737s)
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
backtrace:
[<00000000c0b84cb6>] slab_post_alloc_hook+0x78/0x5b0
[<00000000f76c1d8d>] kmem_cache_alloc_trace+0x16b/0x370
[<000000009f614545>] leak_simple1+0xc0/0x19b [test_kmembugs]
[<00000000747f9f09>] dbgfs_run_testcase+0x1e6/0x51a [test_kmembugs]
[...]
我们来像侦探一样解读这段日志:
-
地址和大小:
0xffff8880127f8000,这块内存大小是 2048 字节。 等等,我们代码里明明申请的是 1520 字节,为什么是 2048? 这就是 slab 分配器的「取整」逻辑。内核会找一个最接近且大于请求大小的缓存,这里是kmalloc-2k,所以给的是 2KB。 -
上下文:
comm "run_tests",pid 5498。 这是告诉你是谁干的。age字段告诉我们这个泄漏已经存在了 84 秒多。 -
内存内容:hex dump 部分全是
0x00。 这是因为我们用的是kzalloc(zeroed alloc),内容被清零了。如果我们用的是普通的kmalloc,这里可能会残留一些之前的数据,那才是真正令人兴奋(或惊恐)的时刻。 -
调用栈(Backtrace)——这是最硬核的证据。 从下往上看:
__x64_sys_write:这是用户空间发起了系统调用(我们的echo命令)。dbgfs_run_testcase:进入了我们的 debugfs 处理函数。leak_simple1:进入了那个故意写坏的测试函数。kmem_cache_alloc_trace:最终执行了 slab 分配。
铁证如山。kmemleak 不止抓住了它,还把它的祖宗十八代都抖出来了。
至于第二个泄漏(vmalloc 那个),报告里也有,逻辑完全一样,栈里会显示 vmalloc 的调用路径。
测试用例 3.2:甩锅给调用者
有时候泄漏不是那么明显。比如这种写法:
// caller
else if (!strncmp(udata, "3.2", 4)) {
res2 = (char *)leak_simple2();
pr_info(" res2 = \"%s\"\n", res2 == NULL ? "<whoops, it's NULL>" : (char *)res2);
if (0) /* test: ensure it isn't freed by us, the caller */
kfree((char *)res2);
}
被调用的 leak_simple2 乖乖地分配了内存并返回指针:
char *leak_simple2(void)
{
char *p = kmalloc(8, GFP_KERNEL);
if (!p) return NULL;
strcpy(p, "leaky!!");
return p;
}
这在 C 语言里是个经典的灰色地带:函数文档里写着「调用者负责释放」,但如果调用者忘了?编译器不会提醒你,运行时也不会当场崩溃。
kmemleak 怎么看这事?
跑一下用例 3.2,扫描,看报告:
unreferenced object 0xffff8880074b5d20 (size 8):
comm "run_tests", pid 5779, jiffies 4298012622 (age 181.044s)
hex dump (first 8 bytes):
6c 65 61 6b 79 21 21 00 leaky!!.
backtrace:
[<00000000c0b84cb6>] slab_post_alloc_hook+0x78/0x5b0
[<00000000f76c1d8d>] kmem_cache_alloc_trace+0x16b/0x370
[<000000009f614545>] leak_simple2+0xc0/0x19b [test_kmembugs]
[<00000000747f9f09>] dbgfs_run_testcase+0x1e6/0x51a [test_kmembugs]
看到了吗?
Hex dump 里甚至把那个字符串 leaky!! 给打出来了。
kmemleak 才不管你是「设计上由调用者释放」还是「忘了释放」,只要没人指着它,就是孤儿,就是泄漏。
这也是为什么内核社区后来推崇 devm_kalloc 这种资源管理机制的原因——把责任从健忘的人身上转移到系统身上。
测试用例 3.3:中断上下文中的幽灵
最后一个场景更刁钻。
前面的例子都是在进程上下文里跑的。如果泄漏发生在中断上下文呢?毕竟中断处理函数是不能睡眠、不能乱来的。
为了模拟这个场景,我们用了一个叫 irq_work 的机制,强制让代码在 hardirq 上下文里跑。
void leak_simple3(void)
{
pr_info("testcase 3.3: simple memory leak testcase 3\n");
irq_work_queue(&irqwork);
}
/* This function runs in (hardirq) interrupt context */
void irq_work_leaky(struct irq_work *irqwk)
{
// ...
pr_debug("kzalloc(129) = 0x%px\n",
kzalloc(129, GFP_ATOMIC)); // Must use GFP_ATOMIC here!
}
跑起来,扫描,看结果:
unreferenced object 0xffff88800b614800 (size 256):
comm "hardirq", pid 0, jiffies 4298048922 (age 12.020s)
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
backtrace:
[...]
[<00000000d54114cc>] irq_work_leaky+0x2a/0x40 [test_kmembugs]
[<00000000c4d72c5e>] irq_work_run_list+0x54/0xf0
[<00000000c4d73044>] irq_work_tick+0x64/0x80
注意这一行:comm "hardirq",以及 pid 0。
这就是 kmemleak 聪明的地方。它不仅发现了泄漏,还准确地指出了泄漏发生的上下文。这对你调试中断处理函数里的内存泄漏简直是救命稻草。
6.4 内核自带的测试模块
其实你不用自己写这些烂代码。内核源码里早就给你准备好了。
如果你配置内核时开启了 CONFIG_DEBUG_KMEMLEAK_TEST,编译系统会生成一个名为 kmemleak-test.ko 的模块(通常在 samples/kmemleak/ 下)。
直接把它插进去:
sudo modprobe kmemleak-test
然后跑一遍 scan。你会被吓一跳的——它一口气报告了 13 个内存泄漏。这是一个很好的验证环境,你可以看看 kmemleak 是怎么一口气处理这么多不同类型的泄漏的。
6.5 控制台上的指挥棒
最后,总结一下那个神奇的 /sys/kernel/debug/kmemleak 文件。我们不仅能读它,还能往里写东西来控制它:
| 写入内容 | 效果 |
|---|---|
scan | 立即触发一次内存扫描。 |
clear | 清空当前的泄漏报告列表。 |
dump=... | 这是一个高级玩法。你可以指定一个地址,让 kmemleak 把这个地址周围的引用关系打印出来。这就像是在问:「这块内存到底被谁指着?」 |
off | 彻底关掉 kmemleak 扫描线程。 |
on | 重新打开扫描线程。 |
一个推荐的调试工作流:
# 1. 清理现场
echo clear > /sys/kernel/debug/kmemleak
# 2. 跑你的测试模块或操作
...
# 3. 稍微等一下(让内存分配稳定下来)
# 4. 触发扫描
echo scan > /sys/kernel/debug/kmemleak
# 5. 检查日志有没有 "new suspected memory leaks"
dmesg | tail
# 6. 如果有,看详细报告
cat /sys/kernel/debug/kmemleak
这套流程虽然简单,但它能帮你省去那些盯着 slabinfo 瞎猜的夜晚。
kmemleak 并不是完美的(它有误报,也有性能开销),但在那漆黑的内核内存空间里,它绝对是你手里最亮的一盏灯。