1. 为什么你需要关注 /proc/vmallocinfo第一次在服务器上看到/proc/vmallocinfo的时候我正被一个内核模块的内存泄漏问题折磨了整整两天。free显示内存充足slabtop也看不出异常但系统就是越来越慢最后 OOM Killer 开始随机杀进程。后来一位做内核开发的朋友提醒我“去看看 vmalloc 区域吧那里面藏着很多 slab 看不到的东西。”从那以后/proc/vmallocinfo就成了我排查内核内存问题的第一站。这个文件到底是个什么东西简单说它是 Linux 内核暴露出来的一个虚拟内存分配信息快照专门记录通过vmalloc系列接口分配的那部分内核虚拟地址空间。和kmalloc走 slab 分配器不同vmalloc分配的内存虚拟地址连续但物理地址不一定连续所以它特别适合那些需要大块连续虚拟地址、但对物理连续性没要求的场景比如加载内核模块、映射硬件寄存器、创建大页映射等。你可能会问/proc/meminfo里不是有VmallocTotal、VmallocUsed吗为什么还要看这个文件原因很简单——meminfo只给你一个总数而/proc/vmallocinfo给你的是每一笔分配的详细清单谁分配的、分配了多大、虚拟地址范围是多少、物理页用了多少、调用者是谁。这就像你查账meminfo是余额vmallocinfo是流水排查问题当然要看流水。这篇文章适合哪些人看如果你在做内核模块开发、驱动调试、系统性能优化或者只是单纯对 Linux 内存管理好奇那这篇内容应该能帮到你。我会从它的输出格式讲起然后逐字段拆解含义再结合几个我实际踩过的坑把排查思路和操作步骤完整地分享出来。全程不堆砌术语尽量用生活化的类比把原理说清楚。提示阅读本文需要你对 Linux 内核内存管理有最基础的了解比如知道什么是虚拟地址、什么是物理页。如果这些概念还比较模糊建议先补一下基础再回来看实操部分会更顺畅。2. /proc/vmallocinfo 的输出格式逐字段拆解2.1 一行记录到底在说什么先看一条典型的输出我直接从手头一台测试机上抓的0xffffc90000000000-0xffffc90000002000 8192 module_alloc0x5c/0x90 pages1 vmap 0xffffc90000002000-0xffffc9000000a000 32768 bpf_jit_alloc_exec0x1a/0x30 pages7 vmap 0xffffc9000000a000-0xffffc9000001a000 65536 pcpu_get_vm_areas0x0/0x5e0 pages15 vmalloc每一行就是一笔 vmalloc 分配记录从左到右依次是虚拟地址范围0xffffc90000000000-0xffffc90000002000起始地址到结束地址。两个地址相减就是这笔分配占用的虚拟空间大小比如第一条就是0x2000即 8192 字节。分配大小紧跟在地址范围后面的数字单位是字节。注意这个大小通常大于你请求的字节数因为内核会做页对齐还可能加上 guard page。调用者信息module_alloc0x5c/0x90这种格式表示是哪个函数发起的分配0x5c是偏移量/0x90是该函数的总长度。这个信息来自内核的符号表能直接告诉你“谁干的”。pagesN这笔分配实际映射了多少个物理页。一个页通常是 4KB所以pages1就是 4KBpages7就是 28KB。分配类型标记最后的vmap、vmalloc、vmap等表示具体用的是哪种映射方式。常见的有vmalloc、vmap、vmap_pages_range、ioremap等。我刚开始看的时候有个疑惑为什么分配大小和 pages 乘以 4K 对不上比如第二条分配大小 32768 字节pages7 即 28672 字节差了 4096 字节。这多出来的一个页就是guard page用来防止越界访问属于内核的安全机制。这个细节在后面排查越界问题时特别有用。2.2 虚拟地址范围里的门道地址范围这一列很多人扫一眼就过去了其实里面信息量很大。Linux 内核的 vmalloc 区域通常位于0xffffc90000000000开始的一段空间具体起始地址因架构和配置而异x86_64 上常见的就是这个范围。相邻两条记录的地址如果是连续的说明它们可能是同一批分配如果中间有空洞那可能是已经释放但还没被复用或者是 guard page 占着。我习惯用一个小技巧把地址范围按起始地址排序然后看相邻记录之间有没有“缝隙”。如果发现某段地址长期空着而系统又在频繁分配 vmalloc那可能是碎片化问题。这个在后面讲排查技巧时会展开。另外地址范围还能帮你判断是否发生了地址复用。比如你怀疑某个模块泄漏可以间隔一段时间抓两次快照对比同一调用者的地址范围是否在变化。如果地址一直在往后涨那基本可以确定是只分配不释放。2.3 调用者信息怎么读才不迷路调用者信息是/proc/vmallocinfo最有价值的部分但也是最容易让人困惑的。module_alloc0x5c/0x90这种格式module_alloc是函数名0x5c是当前指令在函数内的偏移/0x90是函数的总长度。如果你拿到一个不认识的函数名可以用grep在内核源码里搜或者用faddr2line工具把地址转成源码行号。我实际排查时遇到过一个情况调用者显示的是__vmalloc_node_range0x...这是 vmalloc 的内部实现函数说明真正的调用者被内联或者被中间层挡住了。这时候就需要结合dmesg或者模块加载日志来交叉定位。还有一种情况是调用者显示为0x0或者空白那通常是早期启动阶段分配的符号表还没完全建立。注意如果你在容器里看这个文件看到的可能是宿主机的全局信息因为 vmalloc 区域是内核全局的不隔离。这一点在做容器化环境排查时要特别小心别把别人的分配算到自己头上。2.4 pages 和分配类型的对应关系pagesN这个字段看起来简单但结合分配类型一起看能推断出不少东西。比如vmap类型通常用于把已经存在的物理页映射到虚拟地址空间常见于ioremap或者模块加载vmalloc类型则是直接分配新的物理页再映射。我整理了一个简单的对照表方便快速判断分配类型典型调用者常见场景是否消耗物理内存vmalloc__vmalloc_node_range内核模块大块内存是vmapvmap / ioremap映射硬件寄存器、DMA缓冲区否映射已有页vmap_pages_rangeremap_pfn_range驱动 mmap 到用户空间否ioremapioremap_prot映射设备 MMIO 区域否这个表不是绝对的但能帮你快速缩小排查范围。比如你发现物理内存没怎么涨但 vmalloc 区域一直在扩大那大概率是vmap类型的映射泄漏而不是真正的内存分配泄漏。3. 从内核源码看 vmalloc 的分配逻辑3.1 vmalloc 和 kmalloc 的本质区别要真正理解/proc/vmallocinfo得先搞清楚 vmalloc 和 kmalloc 的区别。我用一个生活类比来解释kmalloc 就像你去便利店买一瓶水货架上的水是连续摆放的你拿走一瓶剩下的还是连续的vmalloc 就像你去仓库提货仓库里货物是散放的你只需要一个连续的“提货单号”虚拟地址实际货物可以从不同货架凑。技术上的区别是kmalloc 保证物理地址连续所以能直接用于 DMA 等场景但最大分配尺寸受限且容易产生碎片vmalloc 只保证虚拟地址连续物理页可以离散所以能分配很大的块但访问效率略低因为 TLB 命中率可能下降且不能直接用于 DMA。内核里 vmalloc 的核心实现是__vmalloc_node_range它做几件事从 vmalloc 地址空间里找一段空闲的虚拟地址范围然后逐个分配物理页最后建立页表映射。/proc/vmallocinfo就是在这些步骤完成后把信息记录到vmap_area结构里再通过 seq_file 接口输出。3.2 vmap_area 结构和信息记录时机每个 vmalloc 分配在内核里对应一个struct vmap_area它挂在红黑树和链表上方便快速查找和遍历。这个结构里保存了虚拟地址起止、标志位、以及一个指向struct vm_struct的指针。vm_struct里则有物理页数组、调用者地址、分配类型等信息。/proc/vmallocinfo的 show 函数会遍历这些vmap_area然后调用show_numa_info或者直接打印。调用者信息是通过__builtin_return_address(0)在分配时抓取的保存到vm_struct-caller里。所以如果你看到调用者信息不准可能是编译器优化或者内联导致的。我读过一段内核代码里面有个细节vmalloc分配时会根据gfp标志决定是否立即映射页表。如果是GFP_KERNEL通常会立即映射如果是GFP_NOFS或原子上下文可能延迟映射。这个差异会影响pages字段的准确性——延迟映射时pages可能显示为 0 或者偏小。3.3 为什么有些分配不在 vmallocinfo 里这是很多人会踩的坑明明用了 vmalloc为什么/proc/vmallocinfo里找不到我总结了几种常见情况分配太小如果请求的尺寸小于一个页内核可能直接用 kmalloc 代替不走 vmalloc 路径。使用了 vmalloc_exec 或特殊接口某些架构特定的接口可能不记录到 vmallocinfo。分配失败如果 vmalloc 失败不会有记录但你可能在 dmesg 里看到警告。被过滤了某些内核配置如CONFIG_MMU关闭下vmalloc 行为完全不同。还有一种情况是权限问题非 root 用户看/proc/vmallocinfo可能只能看到部分信息或者干脆看不到。所以排查时记得用 root 或者 sudo。4. 实操用 vmallocinfo 定位内存泄漏4.1 准备工作抓取基线和快照排查内存泄漏的第一步永远是建立基线。我通常会在系统刚启动、业务还没跑起来的时候抓一份快照sudo cat /proc/vmallocinfo /tmp/vmalloc_baseline.txt然后等业务运行一段时间或者怀疑泄漏的时候再抓一份sudo cat /proc/vmallocinfo /tmp/vmalloc_snapshot.txt接下来做差异对比。因为 vmallocinfo 的输出顺序不保证稳定直接 diff 意义不大我习惯先按调用者聚合awk {print $3} /tmp/vmalloc_baseline.txt | sort | uniq -c | sort -rn /tmp/baseline_by_caller.txt awk {print $3} /tmp/vmalloc_snapshot.txt | sort | uniq -c | sort -rn /tmp/snapshot_by_caller.txt diff /tmp/baseline_by_caller.txt /tmp/snapshot_by_caller.txt这样能快速看出哪个调用者的分配次数或总大小在增长。如果某个调用者从 10 次涨到 1000 次那基本就是它了。4.2 计算实际占用别被虚拟大小骗了很多人看到 vmallocinfo 里某条记录大小是 1MB就以为占了 1MB 物理内存。其实不一定。vmap类型的映射可能只是把已有的物理页映射过来并不新增物理内存消耗。真正消耗物理内存的是vmalloc类型以及那些pages字段大于 0 的记录。我一般会这样统计实际物理页消耗awk {for(i1;iNF;i) if($i ~ /^pages/){split($i,a,); suma[2]}} END{print sum*4 KB} /proc/vmallocinfo这个命令把所有pagesN加起来乘以 4KB得到 vmalloc 区域实际映射的物理内存总量。你可以把这个值和/proc/meminfo里的VmallocUsed对比如果差很多说明有大量vmap映射或者统计口径不同。提示VmallocUsed在较新内核里可能不准确因为内核改过统计方式。以 vmallocinfo 里 pages 求和为准更可靠。4.3 一个真实的泄漏排查案例我之前遇到过一个驱动模块加载后系统运行几小时就开始变慢。用上面的方法抓了两次快照发现my_driver_ioctl0x120/0x300这个调用者的分配次数从 5 次涨到了 2000 多次每次分配 64KB。算下来泄漏了 128MB 左右的虚拟空间物理页也涨了差不多 100MB。进一步看地址范围发现这些分配的地址是连续的说明是同一批循环分配没有释放。结合代码审查发现驱动在 ioctl 里对每个请求都调用了vmalloc但只在模块卸载时才释放。修复方法很简单在请求处理完成后立即vfree或者改用内存池复用。这个案例的关键点是vmallocinfo 直接告诉了你调用者省去了用 kmemleak 或者 perf 逐步排查的时间。当然前提是你得能看懂调用者符号并且有基线对比。4.4 结合其他工具交叉验证vmallocinfo 不是万能的有些情况需要结合其他工具slabtop看 kmalloc 和 slab 缓存的情况排除非 vmalloc 泄漏。/proc/meminfo看全局内存趋势确认是内核内存还是用户内存问题。kmemleak如果内核编译时开了CONFIG_DEBUG_KMEMLEAK可以用它自动追踪泄漏点。ftrace如果调用者信息不够明确可以用 ftrace 跟踪__vmalloc_node_range的调用栈。我通常的顺序是先看 meminfo 确认趋势再看 vmallocinfo 定位嫌疑调用者最后用 ftrace 或者代码审查确认根因。这个流程在大多数内核内存问题上都能走通。5. 常见问题与排查技巧实录5.1 为什么 vmallocinfo 输出为空或很少如果你cat /proc/vmallocinfo发现几乎没内容别慌可能是这几种原因内核配置问题CONFIG_MMU没开或者 vmalloc 被禁用。权限不足非 root 用户可能看不到详细内容。系统太干净刚启动的系统vmalloc 分配本来就少。容器环境某些容器运行时可能挂载了空的 proc 文件。排查方法先确认uname -r和内核配置再用sudo试一次。如果还是空检查/proc是否被覆盖挂载。5.2 调用者显示为未知符号怎么办调用者显示0x0或者unknown时通常是因为符号表没加载或者地址被优化了。可以尝试用cat /proc/kallsyms | grep 地址手动查找符号。检查内核是否开启了CONFIG_KALLSYMS。如果是模块确认模块已正确加载且符号已注册。我遇到过一种情况调用者显示的是模块内的静态函数但因为编译优化被内联了符号表里找不到。这时候只能结合代码和偏移量反推。5.3 地址范围重叠或异常怎么处理正常情况下vmalloc 分配的地址范围不应该重叠。如果你看到重叠可能是内核 bug极少数情况下是 vmalloc 分配器的问题。读取时并发cat 文件时如果有分配或释放可能读到不一致的快照。地址复用释放后立即被复用看起来像重叠。处理方法是多抓几次快照确认是否稳定复现。如果稳定重叠建议升级内核或者查内核邮件列表。5.4 性能影响读 vmallocinfo 会不会很慢会。/proc/vmallocinfo的输出需要遍历所有 vmap_area 并解析符号在 vmalloc 分配很多的系统上比如几十万条记录cat 一次可能要几秒甚至更久。我建议不要在高频监控里频繁读取。如果只需要统计信息用awk在读取时直接聚合避免保存大文件。生产环境排查时尽量在业务低峰期操作。注意某些内核版本里读 vmallocinfo 会持有 vmap_area_lock可能短暂阻塞其他 vmalloc 分配。虽然时间很短但在极端高并发场景下要留意。5.5 速查表常见现象与可能原因现象可能原因排查方向VmallocUsed 持续增长某模块只分配不释放对比快照定位调用者pages 总和远小于分配大小大量 vmap 映射检查 ioremap 或驱动 mmap调用者全是内部函数符号被内联或优化用 kallsyms 或 ftrace地址范围碎片化严重频繁分配释放不同大小考虑改用内存池读取时系统卡顿记录太多遍历慢低峰期操作或改用统计接口6. 进阶从 vmallocinfo 延伸到内核内存优化6.1 减少 vmalloc 使用的几种策略如果你发现系统里 vmalloc 分配过多可以考虑这些优化改用 kmalloc 或 slab对于小尺寸、频繁分配的场景kmalloc 更高效。使用内存池预分配一大块 vmalloc内部自己管理减少分配次数。延迟分配对于不急着用的内存用vmalloc的延迟映射特性。调整模块加载方式有些模块可以改成按需加载减少常驻 vmalloc 占用。我实际优化过一个网络驱动把每个包缓冲区的 vmalloc 改成 kmalloc 加 scatter-gathervmalloc 占用直接降了 80%系统吞吐还略有提升。6.2 监控 vmalloc 使用的自动化脚本手动抓快照毕竟麻烦我写了一个简单的监控脚本每 5 分钟记录一次 vmalloc 总量和 Top 10 调用者#!/bin/bash LOG/var/log/vmalloc_monitor.log echo $(date) $LOG awk {for(i1;iNF;i) if($i ~ /^pages/){split($i,a,); suma[2]}} END{print Total pages: sum} /proc/vmallocinfo $LOG awk {print $3} /proc/vmallocinfo | sort | uniq -c | sort -rn | head -10 $LOG这个脚本很粗糙但足够发现趋势。如果某天 Total pages 突然涨了再去细查。6.3 内核版本差异与兼容性注意不同内核版本里/proc/vmallocinfo的格式和字段可能有细微差别。比如老版本可能没有pages字段或者调用者格式不同。我在 4.x 和 5.x 上都用过基本字段一致但 5.8 之后对vmap类型的统计更细了。如果你在写自动化工具建议先检测内核版本再做字段解析。另外某些发行版可能打了补丁输出格式和主线不一样这点在跨环境排查时要特别注意。6.4 一个容易忽略的点vmalloc 与 CMA 的交互在嵌入式或者移动设备上vmalloc 可能和 CMA连续内存分配器有交互。某些驱动会用 vmalloc 映射 CMA 区域这时候 vmallocinfo 里的 pages 字段可能不反映真实的 CMA 占用。如果你在排查这类系统建议同时看/proc/meminfo里的 CmaTotal 和 CmaFree。我踩过一次坑以为 vmalloc 泄漏结果是 CMA 区域被映射后没释放vmallocinfo 里看着正常但 CMA 已经耗尽了。后来加了 CMA 监控才定位到。7. 我个人在实际操作中的体会说了这么多最后分享几点我自己的经验。第一基线比什么都重要。没有基线你看到任何数字都觉得可疑有了基线变化一目了然。我现在的习惯是每台新机器上线前都抓一份 vmallocinfo 存档后面排查时直接对比。第二不要只看总量要看调用者。总量涨了可能是正常业务增长但某个调用者异常增长才是问题。vmallocinfo 最大的价值就是给了你调用者信息别浪费。第三结合代码看。vmallocinfo 告诉你“谁分配了”但不告诉你“为什么分配”。最终定位根因还是要回到代码看分配逻辑是否合理释放路径是否完整。第四注意 guard page 和页对齐。很多人算内存占用时忘了这两个因素导致对不上账。记住分配大小通常大于请求大小pages 乘以 4K 也可能大于分配大小因为有 guard page。这个文件看起来简单但真正用起来能解决大问题。如果你还没用过建议下次遇到内核内存问题时第一时间想到它。如果你已经在用欢迎交流你的排查技巧我也还在不断踩坑和学习。