Linux内核vmallocinfo详解:虚拟内存映射排查与泄漏定位
1. 为什么你需要关注 /proc/vmallocinfo第一次认真翻/proc/vmallocinfo的人多半是被逼的。要么是内核模块加载失败报了个-ENOMEM要么是系统跑着跑着vmalloc区域告急dmesg里刷出一片vmap allocation for size xxx failed要么就是做内存泄漏排查时发现free看着还有余量但某个子系统就是申请不到大块虚拟内存。这时候你翻遍free、/proc/meminfo、slabinfo发现它们讲的都是物理页和 slab 的故事跟vmalloc这块虚拟地址空间没半毛钱关系。而/proc/vmallocinfo就是专门讲这块故事的窗口。简单说/proc/vmallocinfo是 Linux 内核提供的一个 procfs 接口它把内核vmalloc地址空间里所有已经建立的虚拟内存映射逐条列出来包括每一条映射的起始地址、大小、调用者caller、物理页信息、是否可执行、是否属于模块、是否来自ioremap等等。它能帮你回答几个非常具体的问题这块虚拟内存是谁申请的申请了多大背后有没有真的映射物理页是vmalloc还是vmap还是ioremap有没有泄漏——也就是申请了但一直没释放的映射适合读这篇文章的人我大致分三类。第一类是内核驱动开发者尤其是写模块的经常要vmalloc一大片缓冲区出问题时需要定位。第二类是系统运维和性能工程师线上机器出现虚拟内存碎片或者vmalloc空间耗尽需要快速判断根因。第三类是对内核内存管理感兴趣、想动手做实验的学习者/proc/vmallocinfo是一个非常好的“可视化”入口比啃mm/vmalloc.c源码直观得多。我自己的经验是很多所谓“内存泄漏”的锅最后都落到vmalloc头上而/proc/vmallocinfo是唯一能让你一眼看出“谁在偷偷占坑”的工具。下面我按“设计思路—字段解析—实操定位—问题排查”的顺序把这块内容彻底讲透。2. 内核 vmalloc 机制与 vmallocinfo 的设计思路2.1 vmalloc 和 kmalloc 到底差在哪要读懂vmallocinfo先得搞清楚vmalloc在整个内核内存体系里的位置。内核里申请内存主要有两条路kmalloc和vmalloc。kmalloc走的是 slab 分配器拿到的内存物理地址连续虚拟地址也连续适合小对象、DMA 缓冲区、频繁分配释放的场景。但它的短板是物理连续性——当系统跑久了物理内存碎片化想找一大片连续的物理页就变得很难。vmalloc走的是另一条路它只保证虚拟地址连续物理页可以东一页西一页靠页表把它们拼起来。代价是每次访问都要经过页表映射TLB 压力大性能不如kmalloc而且分配粒度至少是一页。所以内核的惯例是小内存用kmalloc大块且不要求物理连续的内存用vmalloc比如模块加载时的代码段、大的软件缓冲区、ioremap映射的设备寄存器。/proc/vmallocinfo记录的就是vmalloc地址空间在 x86_64 上通常是0xffffc90000000000开始的一段区域里的所有映射。它不关心 slab不关心 buddy 系统的空闲链表只关心“这块虚拟地址被谁占了、占了多大”。2.2 为什么内核要暴露这个接口内核开发者很早就意识到vmalloc空间是个稀缺资源。在 32 位时代vmalloc区域只有几百 MB一旦耗尽模块加载、ioremap全部失败系统基本就废了。即便到了 64 位时代虚拟地址空间看似取之不尽但vmalloc区域仍然有边界而且碎片化问题依然存在——你可能有足够的空闲虚拟地址总量但没有足够大的连续虚拟地址段来满足一次大分配。/proc/vmallocinfo的设计目标就是让这个“黑盒”变得透明。它遍历内核里维护vmalloc区域的链表vmap_area_list对每一个vmap_area输出一行。每一行背后其实是一个struct vmap_area加上它关联的物理页信息。这个接口是只读的读取时会持有必要的锁保证遍历过程中链表不会被破坏。注意读取/proc/vmallocinfo本身会有一定开销因为它要遍历整个vmap_area链表并格式化输出。在映射条目极多几万条的机器上cat一次可能耗时几十毫秒甚至更久不建议在高频监控里无脑轮询。2.3 输出格式的演进早期内核的vmallocinfo输出比较简单只有地址范围、大小和调用者。后来逐步加入了物理页信息、ioremap标记、模块归属、可执行属性等。不同内核版本字段略有差异但核心结构稳定。下面给一个典型的输出样例基于常见 5.x/6.x 内核0xffffc90000000000-0xffffc90000002000 8192 vmalloc N02 ... 0xffffc90000002000-0xffffc90000004000 8192 vmalloc N02 ... 0xffffc90000400000-0xffffc90000421000 135168 vmalloc N033 ... 0xffffc90000500000-0xffffc90000501000 4096 ioremap phys0x00000000febf0000 0xffffffffc0000000-0xffffffffc0012000 73728 module ...第一列是虚拟地址范围第二列是大小字节第三列是分配类型后面跟着物理页信息和调用者。理解每一列的含义是使用这个文件的第一步。3. 逐字段拆解 vmallocinfo 输出3.1 地址范围与大小第一列0xffffc90000000000-0xffffc90000002000是这段映射的虚拟地址起止。两者相减就是第二列的大小单位是字节。这里有个细节vmalloc分配会做页对齐所以大小基本都是 4096 的整数倍。如果你看到某个大小不是页对齐的那多半是vmap或者特殊映射。地址范围还能帮你判断这段映射在vmalloc空间里的位置。如果所有映射都集中在低地址段高地址段空着说明碎片化不严重如果映射散布得到处都是中间全是空洞那就是碎片化的典型表现。我排查过一次线上问题vmalloc总量才用了 30%但一次 128MB 的分配死活失败原因就是地址空间被切得七零八落找不到连续的 128MB 虚拟地址段。3.2 分配类型vmalloc / vmap / ioremap / module第三列是这段映射的“来源类型”这是最有价值的信息之一。常见取值有类型含义典型场景vmalloc通过vmalloc()系列申请驱动大缓冲区、内核模块数据vmap通过vmap()映射已有物理页把分散的 page 拼成连续虚拟地址ioremap映射设备物理地址到内核虚拟地址访问 MMIO 寄存器module模块加载时分配的代码/数据段insmod后模块占用的空间user用户空间相关映射较少见特定架构下的特殊映射区分这几类非常关键。比如你发现ioremap条目特别多那可能是某个驱动反复映射设备寄存器却没释放如果module条目在模块卸载后依然存在那基本可以断定模块卸载路径有泄漏。3.3 物理页信息 N0xxN02这种字段表示这段映射背后实际映射了多少个物理页。N0是 node 0 的意思在多 NUMA 节点机器上可能出现N1、N2。这个数字乘以 4096 就是实际映射的物理内存大小。这里有个容易踩的坑虚拟大小和物理页数量不一定对等。vmalloc分配时如果用了__GFP_ZERO或者 lazy 分配策略可能虚拟地址先占上物理页按需才填。所以你会看到某些条目虚拟大小很大但N0很小说明物理页还没真正分配。反过来vmap映射已有物理页时N0会如实反映页数。实操心得排查“虚拟内存够但物理内存不够”还是“物理内存够但虚拟地址不够”时就看这两个数字。虚拟大小大、物理页少是虚拟地址碎片问题两者都大是真实内存占用问题。3.4 调用者 caller 与模块归属行尾通常会打印调用者的符号名比如some_driver_init0x1a/0x100或者模块名。这是定位“谁申请的”最直接的线索。如果符号显示为0xffffffffc0xxxxxx这种裸地址说明调用者所在的模块没有导出符号信息你需要结合/proc/kallsyms或者模块加载地址去反查。在开启CONFIG_DEBUG_VM或者相关调试选项时调用者信息会更完整。生产内核为了性能有时会裁剪掉部分符号信息这时候你只能靠地址范围去推断。4. 动手实操用 vmallocinfo 定位真实问题4.1 环境准备与基础读取先确认你的内核开启了CONFIG_PROC_VMCORE之外的相关配置实际上/proc/vmallocinfo依赖CONFIG_MMU和 procfs绝大多数发行版默认就有。直接读sudo cat /proc/vmallocinfo | head -50如果文件不存在检查内核配置里CONFIG_PROC_FS是否开启。读出来之后先做几个基础统计快速建立整体印象# 总条目数 sudo wc -l /proc/vmallocinfo # 按类型统计条目数 sudo awk {print $3} /proc/vmallocinfo | sort | uniq -c | sort -rn # 总虚拟大小字节 sudo awk {sum $2} END {print sum} /proc/vmallocinfo # 按类型统计虚拟大小 sudo awk {a[$3] $2} END {for (k in a) print k, a[k]} /proc/vmallocinfo这几条命令下来你就能知道当前vmalloc空间被占了多少、主要是哪类映射在占。我一般会先看vmalloc和module两类因为这两类最容易出泄漏。4.2 找出占用最大的映射sudo awk {print $2, $3, $NF} /proc/vmallocinfo | sort -rn | head -20这条命令把大小、类型、调用者三列拎出来按大小排序。排在前面的往往是几个大块缓冲区。如果某个驱动的缓冲区大小异常比如你预期 1MB结果看到 64MB那就要去查它的分配逻辑了。我遇到过一个案例某驱动在每次 ioctl 时都vmalloc一块 4MB 的临时缓冲区正常路径会释放但某个错误分支直接 return 忘了vfree。跑几天后vmallocinfo里堆了几百条同样调用者的 4MB 条目一眼就能看出来。4.3 检测潜在泄漏按调用者聚合sudo awk {print $NF} /proc/vmallocinfo | sort | uniq -c | sort -rn | head -20这条命令按调用者符号聚合统计每个调用者出现了多少次。如果某个调用者出现几百上千次而它本应是“一次性初始化”的函数那基本就是泄漏。正常情况下初始化函数只应该出现一次。更进一步把调用者和总大小一起聚合sudo awk {a[$NF] $2; c[$NF]} END {for (k in a) print c[k], a[k], k} /proc/vmallocinfo | sort -rn | head -20输出里次数多、总大小大的调用者就是重点怀疑对象。4.4 观察 vmalloc 空间碎片化碎片化不能只看总量要看地址分布。把地址范围提取出来看看空洞情况sudo awk {print $1} /proc/vmallocinfo | sort | head -5 sudo awk {print $1} /proc/vmallocinfo | sort | tail -5对比最低地址和最高地址再结合总大小就能估算碎片程度。如果最低地址和最高地址跨度极大比如跨了几个 GB但总占用只有几百 MB说明地址空间被严重打散。这时候即便总空闲量足够大块连续分配也可能失败。注意vmalloc空间的碎片化不像物理内存那样有成熟的规整机制。内核有vmap_area的延迟回收和合并逻辑但效果有限。真正要缓解只能从源头减少频繁的小块vmalloc/vfree。5. 常见问题与排查技巧实录5.1 vmalloc 分配失败但内存充足这是最经典的场景。dmesg报vmalloc: allocation failure: xxx bytes但free -h显示内存大把。原因通常有两个一是vmalloc虚拟地址空间碎片化找不到连续段二是vmalloc区域本身有上限某些架构或配置下。排查步骤先看/proc/vmallocinfo的地址分布确认碎片程度再看/proc/meminfo里的VmallocTotal、VmallocUsed、VmallocChunk。VmallocChunk表示当前最大的连续可用块如果它远小于你要分配的大小那就是碎片问题。grep -i vmalloc /proc/meminfo5.2 模块卸载后映射未释放模块卸载后如果/proc/vmallocinfo里还有该模块的条目说明卸载路径有泄漏。常见原因是模块里用了vmalloc但exit函数没vfree或者用了vmap没vunmap。排查方法加载模块前记录一次vmallocinfo卸载后再记录一次做 diffsudo cat /proc/vmallocinfo /tmp/before.txt # 加载并卸载模块 sudo cat /proc/vmallocinfo /tmp/after.txt diff /tmp/before.txt /tmp/after.txt如果 diff 里还有残留条目顺着调用者符号去查代码。5.3 ioremap 条目异常增多ioremap泄漏也很常见尤其是驱动在每次打开设备时都ioremap寄存器但关闭时忘了iounmap。表现就是ioremap条目数随设备打开次数线性增长。sudo awk $3 ioremap /proc/vmallocinfo | wc -l反复打开关闭设备观察这个数字是否增长。如果是去查驱动的open/release路径。5.4 常见问题速查表现象可能原因排查命令vmalloc 分配失败虚拟地址碎片化grep VmallocChunk /proc/meminfo模块卸载后条目残留vfree/vunmap 遗漏diff 前后 vmallocinfoioremap 条目持续增长iounmap 遗漏统计 ioremap 条目数某调用者条目异常多循环内分配未释放按调用者聚合统计虚拟大小大但物理页少lazy 分配或未触碰看 N0 字段读取 vmallocinfo 很慢条目过多wc -l确认条目数5.5 几个容易忽略的坑第一个坑是符号信息缺失。生产内核可能裁剪了调用者符号你看到的是一堆裸地址。这时候要结合/proc/kallsyms和模块加载基址去反查或者临时换一个带调试符号的内核复现。第二个坑是读取时机。vmallocinfo是瞬时快照如果你在分配和释放之间读取可能看到中间状态。排查泄漏要多次读取对比不能只看一次。第三个坑是NUMA 影响。多节点机器上N0、N1的分布能反映内存分配的 NUMA 倾向。如果所有物理页都集中在 node 0而进程跑在 node 1性能会受影响。这时候要去看驱动的分配标志有没有指定节点。第四个坑是和 vmalloc 参数混淆。内核启动参数里有vmalloc可以调整vmalloc区域大小但改这个参数需要重新规划地址空间布局不是随便加的。改之前一定要确认架构和内核版本的支持情况。6. 把 vmallocinfo 用成日常工具我现在的习惯是只要碰到内核内存相关问题第一反应就是先cat /proc/vmallocinfo扫一眼。它不像slabinfo那么庞大也不像meminfo那么宏观它刚好卡在“虚拟地址映射”这个粒度上信息量和可读性平衡得很好。如果你要把它做成监控项建议采集这几个指标总条目数、总虚拟大小、VmallocChunk、按类型的条目数和大小、Top N 调用者。采集频率不要太高几分钟一次足够因为vmalloc的变化通常不是秒级的。采集脚本用 awk 就够了不需要上复杂的工具。最后分享一个我常用的小组合vmallocinfo配合slabinfo和meminfo一起看。meminfo告诉你物理内存全局情况slabinfo告诉你内核小对象占用vmallocinfo告诉你大块虚拟映射。三者交叉基本能覆盖内核内存问题的大部分场景。真正定位到具体代码行还是得靠调用者符号加源码审查但vmallocinfo能帮你把范围从“整个内核”缩小到“某几个函数”这个价值就已经非常大了。