
1. 从一次内存异常排查说起为什么我会盯上 /proc/vmallocinfo第一次真正把/proc/vmallocinfo当回事是因为一台跑了半年多的服务器出现了诡异现象free -m显示物理内存还剩不少但内核模块加载开始随机失败dmesg里偶尔蹦出vmalloc: allocation failure的字样。当时第一反应是内存泄漏可slabtop、/proc/meminfo翻了个遍都没找到明显异常。后来一位做内核开发的朋友提醒我你去看一眼/proc/vmallocinfo问题大概率在 vmalloc 区域。这句话点醒了我。我们平时排查内存问题习惯性盯着用户态进程的 RSS、盯着 slab 分配器、盯着 page cache却经常忽略内核虚拟地址空间中一块非常特殊的区域——vmalloc 区。而/proc/vmallocinfo就是观察这块区域最直接的窗口。这篇文章我想把/proc/vmallocinfo这个东西彻底讲透。它是什么、每一列代表什么、内核里 vmalloc 的分配机制是怎样的、怎么用它定位内存泄漏和碎片问题、和kmalloc/slab的边界在哪里、生产环境里有哪些坑。内容会偏底层但只要你对 Linux 内存管理有一点基础概念跟着读下来应该能上手。适合做内核开发、驱动开发、系统运维、性能调优的读者也适合那些被内存明明够却分配失败折磨过的同行。需要先说明一点vmalloc 这块区域的行为和内核版本、架构、配置项比如CONFIG_VMAP_STACK、CONFIG_HAVE_ARCH_HUGE_VMAP关系很大我下面讲的内容以常见的 x86_64 4.x/5.x/6.x 内核为主其他架构细节会有差异遇到具体问题还是要以你手头内核的实际行为为准。2. vmalloc 到底解决什么问题和 kmalloc、slab 的边界2.1 物理连续 vs 虚拟连续一个生活化类比要理解/proc/vmallocinfo先得理解 vmalloc 存在的意义。内核里分配内存有两条主要路径kmalloc和vmalloc。kmalloc分配的是物理连续的内存。你可以把它想象成在停车场里找一块连续的车位必须从头到尾挨着。物理连续的好处是 DMA 可以直接用、缓存友好、访问快但坏处也很明显当内存碎片化严重时想找一大块连续的物理页会越来越难哪怕总空闲内存很多也可能因为凑不出连续块而失败。vmalloc分配的是虚拟连续、物理离散的内存。类比一下你需要的是一串连续的门牌号虚拟地址连续但每个门牌号对应的房子可以散落在城市各处物理页离散。内核通过页表把这段虚拟地址一段段映射到零散的物理页上。代价是每次访问都要经过页表翻译TLB 命中率低性能比 kmalloc 差好处是几乎不受物理碎片影响只要虚拟地址空间够、物理页总数够就能分配成功。所以内核里的分工很清晰小对象、对性能敏感的、需要 DMA 的用kmalloc/slab大块、不要求物理连续、生命周期较长的用vmalloc。典型场景包括加载内核模块时代码段和数据段的映射、ioremap的某些实现、大数组缓冲区、vzalloc出来的零初始化大内存等。2.2 vmalloc 地址空间长什么样在 x86_64 上内核虚拟地址空间被划分成若干区域。vmalloc 区通常位于直接映射区direct map之上、内核 text 区之下具体起止地址由VMALLOC_START和VMALLOC_END决定不同内核版本和配置下数值不同。你可以通过/proc/kallsyms或者内核源码里的arch/x86/include/asm/pgtable_64_types.h找到这些宏。vmalloc 区的大小是有限的。在 4 级页表的 x86_64 上vmalloc 区大概有 32TB 量级具体取决于内核配置听起来很大但如果你频繁分配释放、产生碎片或者某个驱动疯狂泄漏这块空间也会被耗尽。一旦耗尽新的 vmalloc 分配就会失败表现就是前面说的内存明明够却分配不了。这里有个关键点vmalloc 区的耗尽和物理内存耗尽是两回事。/proc/meminfo里的VmallocTotal、VmallocUsed、VmallocChunk三个字段就是专门描述这块区域的。VmallocUsed是已使用的量VmallocChunk是当前最大的连续空闲块。如果VmallocChunk变得很小即使VmallocUsed远没到VmallocTotal也可能因为碎片而分配失败。2.3 为什么 /proc/vmallocinfo 不可替代有人会问/proc/meminfo里已经有 vmalloc 的统计了为什么还要/proc/vmallocinfo因为/proc/meminfo只给你总量不告诉你是谁占的。当VmallocUsed持续增长时你根本不知道是哪个模块、哪个子系统在泄漏。而/proc/vmallocinfo会列出每一个 vmalloc 分配区域的详细信息起始地址、大小、调用者、是否可执行、物理页数量等。这就相当于从这个月花超了细化到每一笔账单的明细排查泄漏时价值巨大。我个人的经验是只要怀疑 vmalloc 相关的问题第一件事就是cat /proc/vmallocinfo然后按大小排序、按调用者聚合基本能快速锁定嫌疑对象。3. 逐列拆解 /proc/vmallocinfo 的输出格式3.1 一行典型输出长什么样先看一行真实的输出数值做了脱敏处理但格式是真实的0xffffc90000000000-0xffffc90000003000 12288 ioremap0x1a/0x30 phys0x00000000feb00000 ioremap这一行信息量其实很大我们逐段拆。第一段0xffffc90000000000-0xffffc90000003000是这段 vmalloc 区域的虚拟地址起止范围。两个地址相减就是这段区域的虚拟大小这里是0x3000即 12288 字节12KB。第二段12288是字节数和地址相减的结果一致。注意这个值可能比实际请求的大小略大因为 vmalloc 分配会做页对齐并且可能因为 guard page 而多占一页。第三段ioremap0x1a/0x30是调用者信息。格式是函数名偏移/函数总长度。ioremap0x1a/0x30表示这次分配是在ioremap函数内、偏移0x1a处发起的该函数总长0x30。这个信息是内核通过__builtin_return_address之类的机制抓取的调用栈顶能帮你定位到具体代码位置。第四段phys0x00000000feb00000是物理地址。只有部分分配会显示这个字段比如ioremap这类明确映射到已知物理地址的。普通的vmalloc分配通常不显示phys因为物理页是离散的没法用一个地址表示。第五段ioremap是分配类型标签。常见的标签有vmalloc、vzalloc、vmap、ioremap、module_alloc、bpf等代表这次分配走的是哪条路径。3.2 各字段的深层含义与常见误读很多人第一次看这个输出会误以为第二段的字节数就是实际占用物理内存。这是错的。vmalloc 分配的是虚拟地址物理页是按需映射的。对于vzalloc出来的区域物理页在分配时就已经落实但对于某些 lazy 映射的场景物理页可能还没真正分配。所以虚拟大小不等于物理占用。另一个常见误读是调用者信息。函数名偏移/长度里的函数名是直接调用__vmalloc那一层的函数不一定是最终的业务代码。比如很多驱动通过vmalloc包装函数分配你看到的可能是包装函数名需要结合源码再往上追一层。我踩过这个坑一开始盯着一个中间层函数查了半天后来才发现真正的泄漏点在它下面调用的另一个模块里。还有一个细节guard page。vmalloc 分配默认会在区域末尾加一个不可访问的页作为保护防止越界。所以你在输出里看到的大小可能比代码里请求的size多一个页4KB。排查时如果发现我明明只要 8KB怎么显示 12KB别慌这是正常的保护机制。3.3 用表格快速对照字段字段位置含义排查时的用途第1段虚拟地址起止范围判断地址是否落在 vmalloc 区、是否越界第2段字节数排序找大块、聚合算总量第3段调用者函数偏移/长度定位泄漏源头第4段物理地址可选仅 ioremap 等场景出现第5段分配类型标签区分 vmalloc/vzalloc/vmap/ioremap这张表建议收藏排查时对着看效率会高很多。4. 内核里 vmalloc 的分配链路与关键参数4.1 从 vmalloc() 到 __vmalloc_node_range()用户内核开发者调用vmalloc(size)内核内部其实经过了好几层。简化后的链路大致是vmalloc(size)→__vmalloc_node(size, align, GFP_KERNEL, NUMA_NO_NODE, caller)__vmalloc_node→__vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, gfp_mask, prot, 0, node, caller)__vmalloc_node_range负责在 vmalloc 区找一段空闲虚拟地址alloc_vmap_area、分配物理页alloc_pages、建立页表映射map_kernel_rangevzalloc就是vmalloc加上__GFP_ZERO分配出来的内存自动清零。vmap则是把已经存在的物理页映射到一段连续的虚拟地址上常用于把多个分散的 page 拼成一块连续虚拟内存。理解这条链路的意义在于当/proc/vmallocinfo显示某个调用者时你能顺着这条链路反推它到底走的是哪条路径以及可能在哪一步出问题。比如分配失败可能发生在找不到空闲虚拟地址碎片问题也可能发生在物理页分配失败真的没内存了两者的排查方向完全不同。4.2 影响 vmalloc 行为的关键配置有几个内核配置和参数会显著影响 vmalloc 的行为排查前最好确认一下CONFIG_VMAP_STACK开启后内核栈也走 vmalloc 区。这会让 vmalloc 区的分配数量大幅增加/proc/vmallocinfo里会看到大量vmap_stack相关的条目。如果你看到几千个栈映射别惊讶这是正常的。CONFIG_HAVE_ARCH_HUGE_VMAP允许 vmalloc 使用大页映射减少 TLB 压力。开启后大块分配可能以 2MB 页映射/proc/vmallocinfo的物理页统计会不同。CONFIG_DEBUG_VM调试选项会做更多一致性检查排查阶段可以临时开启但生产环境慎用有性能开销。vmalloc内核启动参数可以调整 vmalloc 区大小但一般不建议动除非你非常清楚后果。我遇到过一台机器因为开了CONFIG_VMAP_STACK且线程数极多/proc/vmallocinfo里栈映射条目上万cat一次要好几秒。这种情况下建议用grep过滤或者写脚本聚合别直接全量输出。4.3 分配失败的两种典型原因结合前面的链路vmalloc 分配失败无非两类第一类虚拟地址空间碎片化。vmalloc 区虽然大但经过长时间频繁分配释放会留下很多空洞。当你要分配一块较大的连续虚拟地址时可能找不到足够大的空洞。这时候/proc/meminfo的VmallocChunk会很小而VmallocUsed可能并不高。解决办法通常是减少频繁的小块 vmalloc 分配或者重启相关服务。第二类物理内存真的不够。虚拟地址找得到但alloc_pages拿不到物理页。这时候要看/proc/meminfo的MemFree、MemAvailable以及是否有内存 cgroup 限制。区分这两类是排查 vmalloc 问题的第一步。而/proc/vmallocinfo配合/proc/meminfo基本能帮你快速判断方向。5. 实战用 /proc/vmallocinfo 定位一次内存泄漏5.1 排查思路从总量到明细假设你发现VmallocUsed在几小时内从 200MB 涨到了 2GB怀疑泄漏。我的排查套路是这样的第一步先看总量趋势。写个脚本每隔一段时间记录/proc/meminfo里的VmallocUsed确认是不是真的在单调增长。第二步抓两份/proc/vmallocinfo快照间隔一段时间然后做 diff。增长的部分就是嫌疑区域。第三步对新增条目按调用者聚合看是哪个函数贡献了最多增量。第四步结合内核源码找到那个函数的分配逻辑确认是否有对应的释放路径。这个思路听起来简单但实操中有几个细节决定成败。5.2 快照 diff 的具体做法直接diff两个/proc/vmallocinfo文件是不行的因为地址会变、顺序会变。我的做法是先把每行解析成结构化数据再比对。一个简单的处理脚本思路用 awk 演示实际可以用 Python 更灵活# 抓取快照提取 大小 和 调用者 两列 awk {print $2, $3} /proc/vmallocinfo | sort /tmp/vmalloc_snapshot_1.txt # 间隔一段时间后再抓一次 awk {print $2, $3} /proc/vmallocinfo | sort /tmp/vmalloc_snapshot_2.txt # 对比 diff /tmp/vmalloc_snapshot_1.txt /tmp/vmalloc_snapshot_2.txt但这样只能看到哪些行新增了没法直接看出哪个调用者总量涨了。更实用的做法是按调用者聚合求和awk {sum[$3]$2} END {for (k in sum) print sum[k], k} /proc/vmallocinfo | sort -rn | head -20这条命令会输出按调用者聚合后的总字节数降序排列前 20 名一目了然。跑两次对比增量最大的那个调用者基本就是泄漏源。注意调用者字段里带偏移如foo0x1a/0x30同一个函数的不同调用点会被算成不同的 key。如果你只关心函数级别可以先把偏移去掉再聚合。5.3 一个真实的排查案例脱敏某次一台存储服务器出现 vmalloc 缓慢泄漏VmallocUsed每天涨约 300MB。用上面的聚合方法发现增量集中在某个自定义字符设备的ioctl处理函数上。进一步看源码发现它在每次ioctl时用vmalloc分配一块缓冲区但错误路径上漏了vfree。正常路径没问题只有参数校验失败的分支忘了释放。修复后泄漏消失。这个案例的教训是vmalloc 泄漏往往藏在错误处理路径里。正常流程大家都会写释放但goto err分支容易漏。排查时如果发现泄漏速率和某个操作的频率正相关比如每秒几次 ioctl基本可以锁定是那条路径。另一个经验/proc/vmallocinfo里的调用者信息在开启CONFIG_KALLSYMS时才有意义否则可能只显示地址。生产内核一般都会开但如果你在裁剪过的嵌入式内核上排查先确认这个配置。6. 那些文档不会告诉你的坑与经验6.1 读取 /proc/vmallocinfo 本身的开销/proc/vmallocinfo不是简单的静态文件每次cat都会遍历整个 vmalloc 链表并格式化输出。在 vmalloc 条目很多比如几万条的机器上读一次可能耗时几百毫秒甚至更久而且会持有相关锁。如果你在高频交易或延迟敏感的服务上频繁读取可能引入可观测的抖动。我的建议是排查阶段低频读取比如每 10 秒一次生产环境不要把它接进常规监控的高频采集。如果确实需要长期监控考虑用 eBPF 或者内核 tracepoint 做更轻量的统计。6.2 大页映射让统计看起来不对前面提过CONFIG_HAVE_ARCH_HUGE_VMAP。开启后大块 vmalloc 可能用 2MB 大页映射。这时候/proc/vmallocinfo里显示的物理页数量和实际会有差异VmallocUsed的统计口径也可能和你预期不同。排查时如果发现虚拟大小和物理占用对不上先确认是不是大页在起作用。6.3 模块卸载后的残留正常情况下内核模块卸载时会释放它申请的 vmalloc 内存。但如果模块有 bug或者卸载路径不完整可能留下孤儿区域。/proc/vmallocinfo里这些区域的调用者可能显示为已卸载模块的符号如果 kallsyms 还缓存着或者干脆是地址。遇到这种情况lsmod对比一下当前加载的模块能帮你发现异常。6.4 和 /proc/meminfo 的 VmallocChunk 配合看单独看/proc/vmallocinfo容易迷失在细节里。我的习惯是先用/proc/meminfo的三个字段做宏观判断VmallocTotalvmalloc 区总大小VmallocUsed已使用VmallocChunk最大连续空闲块如果VmallocUsed高但VmallocChunk也还大说明是量的问题如果VmallocUsed不高但VmallocChunk很小说明是碎片问题。两种情况的处理策略完全不同。前者要找泄漏源后者要考虑减少碎片或者重启。6.5 别忽略调用者的偏移信息前面说过调用者格式是函数偏移/长度。这个偏移其实很有用。同一个函数里如果有多个vmalloc调用点偏移能帮你区分是哪一个。排查时如果发现增量集中在某个特定偏移直接去源码里定位那个偏移对应的行往往一击即中。我一般会用objdump或者gdb反汇编那个函数把偏移换算成源码行号。7. 把 /proc/vmallocinfo 用成日常工具的几个建议7.1 建立基线而不是出问题才看最有效的用法是平时就建立基线。在系统健康、负载稳定的时候记录一份/proc/vmallocinfo的聚合结果作为参考。之后定期对比一旦某个调用者的量偏离基线就能提前发现苗头。等到VmallocUsed暴涨再排查往往已经影响业务了。我通常会在新服务上线后第一周每天抓一次聚合快照观察是否稳定。稳定后改成每周一次。这套习惯帮我提前发现过好几次潜在的泄漏。7.2 写一个顺手的聚合脚本前面给的 awk 一行命令够用但如果你经常排查建议写个稍微完整点的脚本支持按调用者聚合、按大小排序、和上一次快照对比、输出增量 Top N。用 Python 写也就几十行。工具顺手了排查效率会高一个档次。7.3 结合其他观测手段交叉验证/proc/vmallocinfo是静态快照看不到分配的时间点和频率。要完整还原问题最好结合perf probe在__vmalloc_node_range上打点看分配频率和调用栈tracepoint如kmem:vmalloc相关取决于内核版本做动态追踪slabtop、/proc/meminfo做交叉验证单一工具都有盲区交叉验证才能得出可靠结论。7.4 关于内核版本的差异要有心理准备不同内核版本里/proc/vmallocinfo的输出格式、字段含义、甚至是否默认开启都可能有差异。比如早期版本可能没有phys字段某些版本调用者信息格式不同。排查前先确认你的内核版本必要时翻一下对应版本的mm/vmalloc.c源码看s_show函数是怎么格式化的。这是最权威的依据比任何二手资料都可靠。说到底/proc/vmallocinfo只是内核暴露出来的一个观察窗口真正解决问题还是要回到 vmalloc 的分配机制和你的代码逻辑上。工具帮你定位理解帮你修复。我在实际使用中最大的体会是不要等到出问题才去看它把它当成日常巡检的一部分很多问题在爆发前就能被摁住。另外看到调用者信息时多留个心眼偏移量往往藏着定位泄漏点的关键线索别只盯着函数名。