ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux内核内存排查利器:/proc/vmallocinfo详解与实战

Linux内核内存排查利器:/proc/vmallocinfo详解与实战 1. 为什么你需要关注 /proc/vmallocinfo第一次在服务器上看到/proc/vmallocinfo这个文件的人大概率是在排查内存泄漏或者内核模块异常的时候。free命令显示内存还够用slabtop看着也正常但系统就是越来越卡这时候有经验的老手会甩给你一句“去看看 vmallocinfo 吧。”然后你cat一下这个文件屏幕上刷出几百行十六进制地址和一堆看不懂的函数名瞬间懵了。这个文件到底是干什么的简单说它是 Linux 内核暴露出来的一个“账本”专门记录内核虚拟地址空间中通过vmalloc系列函数分配的每一块内存。和kmalloc那种物理地址连续的小块分配不同vmalloc分配的是虚拟地址连续、物理地址可以不连续的内存区域通常用于分配较大的缓冲区比如内核模块加载、驱动初始化、一些子系统的池化内存等。/proc/vmallocinfo就是让你能逐条查看这些分配记录的入口。它解决的核心问题是内核虚拟内存分配的可见性。没有它你只能看到/proc/meminfo里一个笼统的VmallocTotal和VmallocUsed根本不知道是谁在用、用了多少、有没有泄漏。对于内核开发者、驱动工程师、系统运维人员来说这个文件是排查内存问题的必备工具。哪怕你只是个刚接触 Linux 的运维新手学会看这个文件也能让你在遇到“内存够但系统卡”的诡异问题时多一条排查路径。我写这篇东西的出发点很简单网上关于/proc/vmallocinfo的资料要么太零散要么直接甩内核源码对实际排查问题帮助有限。我把自己这些年在实际工作中反复翻这个文件的经历整理出来从字段含义到排查思路再到具体案例尽量说透。适合已经会用基本 Linux 命令、想深入理解内核内存行为的读者也适合正在被内存泄漏折磨的同行。2. 先搞懂 vmalloc 和 kmalloc 的本质区别2.1 物理连续与虚拟连续的分野要理解/proc/vmallocinfo必须先搞清楚vmalloc和kmalloc的根本差异。这两个函数都是内核里申请内存的接口但底层机制完全不同。kmalloc分配的内存保证物理地址连续。物理连续意味着 CPU 可以直接通过物理地址访问不需要额外的页表映射转换所以访问速度快适合 DMA 操作和大多数内核数据结构。但物理连续内存是稀缺资源随着系统运行时间增长物理内存碎片化越来越严重想找到一大块连续的物理页越来越难。所以kmalloc通常只用于分配较小的内存块一般不超过几 MB。vmalloc则只保证虚拟地址连续物理地址可以分散在各个不连续的物理页上。内核通过页表把这些分散的物理页映射到一段连续的虚拟地址空间。这样做的好处是即使物理内存碎片化严重只要还有足够的空闲页就能分配出大块虚拟连续内存。代价是每次访问都需要经过页表转换性能略低而且不能用于 DMA因为 DMA 需要物理地址连续。打个比方kmalloc就像在停车场找连续的车位车少的时候容易找车多了就难vmalloc就像给你一个连续的车位编号但实际停车的位置可以分散在停车场各处只要编号连续就行。2.2 什么时候内核会用 vmalloc内核里用vmalloc的场景其实比很多人想象的要多。常见的有内核模块加载模块的代码段、数据段在加载时通过module_alloc分配底层就是vmalloc区域。驱动的大缓冲区比如某些网卡驱动、存储驱动需要分配几 MB 的环形缓冲区用vmalloc更稳妥。内核栈在配置了CONFIG_VMAP_STACK的系统中每个线程的内核栈都是通过vmalloc分配的这是现代内核的默认行为。ioremap 映射把设备物理地址映射到内核虚拟地址空间时底层也会用到vmalloc区域。一些子系统的池化分配比如percpu分配器、bpf的 JIT 代码区等。正因为使用场景多/proc/vmallocinfo里的条目可能非常杂。你可能会看到module_alloc、vmap、ioremap、bpf_jit_alloc_exec等各种调用者的名字。理解这些名字背后的含义是读懂这个文件的第一步。2.3 vmalloc 地址空间的布局在 64 位系统上内核虚拟地址空间非常宽裕vmalloc区域通常位于内核地址空间的一个专门区间。以 x86_64 为例vmalloc区域起始于0xffffc90000000000附近具体值取决于内核配置和版本大小可以达到几十 TB。每个vmalloc分配都会在这个区间里找一段空闲的虚拟地址范围然后建立页表映射。这个区域是有限的虽然看起来很大但如果持续泄漏最终也会耗尽。一旦vmalloc空间耗尽内核会报vmalloc: allocation failure之类的错误严重时导致系统崩溃。所以监控/proc/vmallocinfo不只是看谁在用内存更是预防虚拟地址空间耗尽的重要手段。3. /proc/vmallocinfo 文件格式逐字段拆解3.1 一行记录长什么样先看一个典型的输出行0xffffc90000a00000-0xffffc90000a1c000 114688 module_alloc0x5d/0x1a0 pages28 vmap这一行包含了好几个字段每个都有明确含义。我逐个拆开讲。3.2 地址范围起始和结束0xffffc90000a00000-0xffffc90000a1c000是这块虚拟内存的起始地址和结束地址。两个地址相减就是这块区域的虚拟大小。比如这里0xffffc90000a1c000 - 0xffffc90000a00000 0x1c000换算成十进制是 114688 字节也就是 112 KB。这个地址范围是虚拟地址不是物理地址。你没法直接用它去对应物理内存但可以通过页表反查。实际排查中这个地址范围主要用来判断分配是否对齐、是否有异常大的块。3.3 大小字段字节数紧接着地址范围后面的数字就是这块分配的大小单位是字节。上面例子里的114688就是 112 KB。这个值通常和地址范围算出来的大小一致但有时候会因为对齐原因略有差异。看这个字段的时候要有“量级感”几 KB 到几十 KB 通常是正常的小分配几 MB 到几十 MB 就要留意是谁在用如果看到几百 MB 甚至 GB 级别的单条记录那基本可以确定有问题。3.4 调用者信息谁申请的module_alloc0x5d/0x1a0这一串是调用者信息。module_alloc是函数名0x5d/0x1a0表示在module_alloc函数偏移0x5d处调用了分配函数该函数总长度0x1a0。这个信息来自内核的符号解析能帮你快速定位是哪个子系统申请的。常见的调用者名字包括调用者含义module_alloc内核模块加载vmap通用 vmalloc 映射ioremap设备物理地址映射bpf_jit_alloc_execBPF JIT 编译代码区pcpu_allocper-CPU 分配器alloc_vm_area通用虚拟区域分配__get_vm_area_nodevmalloc 底层分配函数看到不认识的函数名可以用grep在内核源码里搜或者用addr2line工具反查。3.5 pages 字段物理页数量pages28表示这块虚拟内存背后实际映射了 28 个物理页。在 4 KB 页大小的系统上28 页就是 112 KB和前面的字节数对得上。但在某些情况下pages可能小于虚拟大小对应的页数比如ioremap映射设备内存时物理页可能不是常规 RAM 页。这个字段的价值在于它告诉你实际占用了多少物理内存。虚拟大小可能因为对齐而偏大但pages反映的是真实物理页占用。3.6 标志位vmap、user、ioremap 等行尾可能出现的标志包括vmap表示这是一块通过vmap建立的映射物理页已经存在只是建立虚拟映射。user表示这块区域映射到了用户空间通常和ioremap或特殊驱动有关。ioremap表示这是设备内存映射不是常规 RAM。nopage表示没有关联物理页可能是预留区域。这些标志能帮你区分分配类型。比如ioremap的区域不计入常规内存统计排查内存泄漏时可以排除。4. 实操如何高效读取和分析 vmallocinfo4.1 基础读取命令最直接的读取方式就是catcat /proc/vmallocinfo但输出可能很长几百上千行很常见。更好的方式是配合less或重定向到文件cat /proc/vmallocinfo /tmp/vmallocinfo.txt wc -l /tmp/vmallocinfo.txt先看总行数心里有个底。如果超过几千行说明系统里 vmalloc 分配非常频繁需要重点关注。4.2 按大小排序找大块分配排查内存问题时第一步通常是找大块分配awk {print $2, $0} /proc/vmallocinfo | sort -rn | head -20这个命令把每行的第二个字段大小提出来按数字降序排列取前 20 条。这样你能一眼看到最大的分配是谁。我实际用的时候还会加个格式化把字节数转成人类可读的awk {printf %.2f MB %s\n, $2/1024/1024, $0} /proc/vmallocinfo | sort -rn | head -204.3 按调用者聚合统计想知道哪个子系统占用最多可以按调用者名字聚合awk {print $3} /proc/vmallocinfo | sed s/.*// | sort | uniq -c | sort -rn | head -20这个命令提取第三字段去掉偏移部分统计每个调用者出现的次数。出现次数多不一定占用内存多但能反映分配频率。如果要统计每个调用者的总字节数awk {sum[$3]$2} END {for (k in sum) printf %10.2f MB %s\n, sum[k]/1024/1024, k} /proc/vmallocinfo | sort -rn | head -20这个更有价值能直接看出谁占的内存最多。4.4 监控变化两次采样对比单次快照只能看当前状态要判断有没有泄漏需要间隔一段时间采样两次对比差异cat /proc/vmallocinfo /tmp/vm1.txt sleep 60 cat /proc/vmallocinfo /tmp/vm2.txt diff /tmp/vm1.txt /tmp/vm2.txt | head -50如果两次之间新增了大量条目或者某些条目大小持续增长那就说明有泄漏嫌疑。我一般会间隔 5 到 10 分钟采样观察趋势。注意/proc/vmallocinfo的读取本身会短暂持有内核锁在高负载系统上频繁读取可能造成轻微性能抖动。生产环境排查时尽量避开业务高峰或者用taskset绑定到空闲 CPU 上执行。5. 典型排查案例从 vmallocinfo 定位内存泄漏5.1 案例背景系统内存缓慢增长某次在一台运行了较长时间的服务上监控显示VmallocUsed持续增长从最初的几百 MB 涨到了几个 GB。系统没有明显报错但dmesg里偶尔出现vmalloc: allocation failure的警告。free显示物理内存还有余量但 vmalloc 空间已经紧张。5.2 第一步抓取快照并排序先抓一份当前快照cat /proc/vmallocinfo /tmp/vm_before.txt awk {printf %.2f MB %s\n, $2/1024/1024, $0} /tmp/vm_before.txt | sort -rn | head -30发现前几名里有大量bpf_jit_alloc_exec的条目单个不大几十 KB但数量极多加起来占了将近 1 GB。这很不正常因为系统上并没有运行大量 BPF 程序。5.3 第二步按调用者聚合确认awk {sum[$3]$2} END {for (k in sum) printf %10.2f MB %s\n, sum[k]/1024/1024, k} /tmp/vm_before.txt | sort -rn | head -10输出确认bpf_jit_alloc_exec总占用排第一远超其他调用者。问题定位到 BPF JIT 代码区。5.4 第三步分析原因BPF JIT 代码区用于存放编译后的 BPF 程序机器码。正常情况下BPF 程序加载后会占用一块卸载后释放。但如果程序频繁加载卸载而释放路径有问题就会导致代码区只增不减。进一步检查发现某个监控代理在每次采集数据时都会动态加载一个 BPF 程序采集完就卸载。但由于内核版本的一个已知问题卸载时 JIT 代码区没有完全释放导致每次采集都泄漏几十 KB。运行几天后累积到 GB 级别。5.5 第四步验证和解决为了验证我写了个小脚本每隔 30 秒记录一次bpf_jit_alloc_exec的总大小while true; do total$(awk /bpf_jit_alloc_exec/ {sum$2} END {print sum} /proc/vmallocinfo) echo $(date %s) $total sleep 30 done观察几个小时确认总大小呈线性增长。升级内核到修复版本后问题消失。这个案例说明/proc/vmallocinfo不仅能告诉你“谁在用内存”还能通过聚合和趋势分析定位到具体的泄漏点。如果没有这个文件面对VmallocUsed的增长你只能盲猜。6. 常见问题与排查技巧速查6.1 为什么 vmallocinfo 里的总大小和 VmallocUsed 对不上/proc/meminfo里的VmallocUsed统计的是所有 vmalloc 分配的总和但它的计算方式在不同内核版本里有差异。有些版本只统计vmap区域有些包含ioremap还有些会把已释放但未回收的区域算进去。而/proc/vmallocinfo是逐条列出当前活跃的分配理论上更准确。如果你发现两者差异很大先确认内核版本然后检查是否有大量ioremap区域这些通常不计入VmallocUsed。另外/proc/vmallocinfo的读取本身可能因为并发分配而略有偏差但不会差太多。6.2 条目太多看不过来怎么办这是最常见的问题。我的经验是分三步走先看总量wc -l看行数awk求和看总大小。按调用者聚合用前面给的awk命令找出占用最大的几个调用者。针对性细看只关注那几个调用者的条目看地址范围、大小、标志位是否正常。不要试图逐行读完那样效率太低。6.3 如何判断某个分配是否正常判断标准因调用者而异。比如module_alloc每个已加载模块对应几条大小和模块大小相关通常几十 KB 到几 MB。bpf_jit_alloc_exec每个活跃 BPF 程序对应一条数量应该和 BPF 程序数匹配。pcpu_allocper-CPU 分配数量应该和 CPU 核心数相关不会太多。ioremap设备映射数量应该和驱动初始化次数匹配不会持续增长。如果某个调用者的条目数量远超预期或者总大小持续增长就是异常信号。6.4 读取 vmallocinfo 会影响性能吗会有一点。读取这个文件需要遍历内核的 vmalloc 区域链表并持有vmap_area_lock自旋锁。在分配频繁的系统上这个锁竞争可能比较激烈读取操作会短暂阻塞其他分配。所以不要在高频循环里反复读取。生产环境排查时尽量间隔几分钟采样一次。如果系统已经卡顿读取这个文件可能让情况更糟要谨慎。6.5 有没有替代工具有。/proc/vmallocinfo是最底层的接口但有些工具封装了它vmallocinfo脚本一些发行版自带的分析脚本能自动聚合排序。crash工具在分析内核转储时可以用vmalloc命令查看类似信息。drgn更现代的内核调试工具可以编程方式访问 vmalloc 区域。但无论用什么工具底层数据都来自/proc/vmallocinfo理解它的格式是基础。7. 进阶从 vmallocinfo 反推内核行为7.1 通过地址范围判断分配顺序/proc/vmallocinfo里的条目通常按地址升序排列。地址越低分配越早。如果你看到某个调用者的条目地址普遍偏高说明是最近才分配的。这个特性可以用来判断泄漏发生的时间窗口。比如你发现bpf_jit_alloc_exec的条目地址从某个值开始密集出现而那个地址对应的时间点正好是某个服务启动之后那基本可以锁定是那个服务导致的。7.2 结合其他 proc 文件交叉验证单独看 vmallocinfo 有时不够需要结合其他文件/proc/meminfo看VmallocTotal、VmallocUsed、VmallocChunk的整体趋势。/proc/slabinfo排除 slab 分配器的干扰。/proc/modules确认已加载模块列表和module_alloc条目对应。/sys/kernel/debug/tracing用 ftrace 跟踪vmalloc和vfree调用看是否有不配对的分配释放。我通常先用 vmallocinfo 定位到可疑调用者再用 ftrace 跟踪具体调用栈这样能精确到代码行。7.3 内核配置对 vmallocinfo 的影响不是所有内核都完整支持/proc/vmallocinfo。它依赖于CONFIG_PROC_VMCORE或CONFIG_MMU等配置。在一些嵌入式系统或精简内核上这个文件可能不存在或内容不全。另外CONFIG_VMAP_STACK开启后每个线程的内核栈都会出现在 vmallocinfo 里条目数量会大幅增加。这时候要区分“正常的大量条目”和“异常泄漏”不能只看数量。提示如果你在容器环境里看不到/proc/vmallocinfo可能是因为容器挂载了独立的 proc 文件系统或者内核配置裁剪了该功能。需要在宿主机上查看。8. 我个人的实操体会这些年翻/proc/vmallocinfo的次数不少有几个体会比较深。第一不要孤立地看这个文件。它只是内核内存视图的一个切面必须结合meminfo、slabinfo、dmesg一起看。单独看 vmallocinfo 很容易误判比如把正常的模块加载当成泄漏。第二聚合分析比逐行看有效得多。几百行输出里真正有问题的可能就那几条。用awk聚合能快速缩小范围把精力集中在可疑调用者上。第三趋势比快照重要。单次快照只能看当前状态间隔采样对比才能发现泄漏。我一般会写个简单脚本每隔几分钟记录一次关键调用者的总大小画成趋势图一眼就能看出问题。第四内核版本差异要留意。不同内核版本里vmallocinfo 的字段格式、调用者名字、统计方式都可能有变化。排查前先确认内核版本查对应的文档或源码避免用旧经验套新系统。最后分享一个小技巧如果你怀疑某个模块泄漏可以先用rmmod卸载它再看 vmallocinfo 里对应的module_alloc条目是否消失。如果没消失说明释放路径有问题。这个操作在测试环境里很有效生产环境要谨慎因为卸载模块可能影响业务。这个文件后续还可以这样扩展结合perf或eBPF做实时监控当 vmalloc 分配超过阈值时自动告警或者写个解析脚本把 vmallocinfo 输出转成火焰图直观展示各调用者的内存占用比例。这些我都试过效果不错有机会再单独整理。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进