ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Linux内核vmallocinfo实战:从内存泄漏排查到vmap_area原理

Linux内核vmallocinfo实战:从内存泄漏排查到vmap_area原理 1. 从一次内存泄漏排查说起为什么我要啃 /proc/vmallocinfo内核模块跑着跑着free看物理内存还剩不少但系统就是开始报vmalloc: allocation failure新进程 fork 不出来连ls都卡。这种场景我遇到过不止一次最后把我按在/proc/vmallocinfo前面盯了整整一个下午。如果你也在做内核模块开发、驱动调试或者单纯是个喜欢把系统底裤翻出来看的运维这个文件值得你花时间吃透。/proc/vmallocinfo是 Linux 内核通过 procfs 暴露出来的一个只读接口专门用来展示内核虚拟地址空间中vmalloc 区域的分配情况。注意这里的关键词是虚拟地址空间和vmalloc 区域它跟/proc/meminfo、/proc/slabinfo完全不是一回事。meminfo告诉你物理内存大盘slabinfo告诉你 slab 分配器的对象缓存而vmallocinfo告诉你的是内核里那些不要求物理连续、只要求虚拟连续的内存到底被谁、在哪个调用栈、分配了多大、映射了多少页。它能解决什么问题最典型的三类第一定位 vmalloc 虚拟地址空间的泄漏也就是谁在不停地vmalloc却不vfree第二排查vmalloc失败因为 vmalloc 区域是有大小上限的x86_64 上通常几十 TB 的虚拟空间但受vmap_area结构和碎片影响实际可用会缩水第三审计内核模块的内存行为看看某个驱动是不是偷偷申请了一大片。适合谁看内核驱动开发者、做嵌入式 BSP 的工程师、系统性能调优的人以及任何需要回答这块内存到底谁申请的这个问题的人。我写这篇东西的出发点很简单网上讲vmallocinfo的文章要么只贴一行输出让你自己猜要么把vmap_area结构体一贴就完事真正告诉你这一列数字怎么算出来的为什么两个模块的调用栈长得不一样看到什么值该警觉的内容少得可怜。下面我按自己实际排查的思路把这个文件从格式到原理到实战完整拆一遍。2. 逐列拆解一行 vmallocinfo 到底在说什么先看一行真实的输出长什么样。随便找台 Linux 机器执行sudo cat /proc/vmallocinfo | head你会看到类似这样的内容0xffffc90000000000-0xffffc90000004000 16384 module_alloc0x5d/0x90 pages3 vmap 0xffffc90000004000-0xffffc9000000a000 24576 bpf_prog_alloc0x3e/0x90 pages5 vmap 0xffffc9000000a000-0xffffc90000012000 32768 pcpu_create_chunk0x1a0/0x3a0 pages7 vmap 0xffffc90000012000-0xffffc9000001c000 40960 gen_pool_add_owner0x6a/0x90 pages9 vmap每一行代表一个 vmalloc 区域准确说是vmap_area的分配记录。别急着往下翻我们一列一列抠。2.1 地址范围列虚拟地址的起止边界第一列0xffffc90000000000-0xffffc90000004000是这个分配占用的虚拟地址区间左闭右开。用结束地址减起始地址得到的就是这个区域的虚拟大小这里是0x4000即 16384 字节。这一列的价值在于你可以直接看出虚拟地址空间里哪些区间被占了、有没有空洞、碎片化到什么程度。如果两个相邻区域的地址不连续中间那段就是没被使用的虚拟地址空洞。这里有个容易踩的坑虚拟地址连续不代表物理地址连续。vmalloc 的设计初衷就是虚拟连续、物理离散所以你在这一列看到的连续区间背后对应的物理页可能散落在内存各处。这也是为什么 vmalloc 分配比 kmalloc 慢——它需要逐页建立页表映射。2.2 大小列字节数但别全信第二列16384是这个区域的大小单位字节。但我要提醒一句这个数字是虚拟大小不是实际物理内存占用。对于带vmap标记的区域它通常等于pages乘以页大小但对于某些特殊映射比如ioremap虚拟大小和物理占用可能对不上。所以看这一列时永远要结合后面的pages一起看。2.3 调用栈列谁申请的一目了然第三列module_alloc0x5d/0x90是分配时的调用者信息格式是函数名偏移/函数总长度。这是整个文件里最有价值的一列因为它直接告诉你这块内存是谁申请的。module_alloc说明是加载内核模块时分配的bpf_prog_alloc说明是 BPF 程序占的pcpu_create_chunk是 per-CPU 分配器的块。不过要注意这个调用栈信息是分配时刻记录的不是实时的。如果某个函数被内联了或者编译器做了优化你看到的函数名可能不是最内层的那个。我遇到过__vmalloc_node_range被内联后调用栈只显示到外层函数的情况这时候就得靠偏移量去反查。2.4 pages 与 vmap 标记物理页数和映射类型pages3表示这个区域映射了 3 个物理页。在 4KB 页的系统上3 页就是 12288 字节但前面虚拟大小是 16384 字节为什么对不上因为 vmalloc 分配会做对齐虚拟大小往往大于实际物理页总和。这个差值就是虚拟地址浪费在碎片化严重时这个浪费会累积。vmap标记表示这个区域是通过vmap()建立的映射通常用于把已经存在的物理页映射到虚拟地址空间。没有这个标记的一般是vmalloc直接分配的。还有ioremap标记的那是设备内存映射跟普通内存分配要区别对待。2.5 那些不常见的标记user、ioremap、vpages除了vmap你还可能看到user用户空间映射比如get_user_pages相关、ioremap设备 MMIO 映射、vpages多页映射。这些标记决定了这块内存的性质排查时不能一视同仁。比如ioremap的区域你vfree是没用的得用iounmap。3. vmallocinfo 背后的内核机制vmap_area 与红黑树光会看输出不够要真正理解这个文件得知道内核是怎么管理这些区域的。这一节我们钻进内核源码层面把vmallocinfo的数据来源讲清楚。3.1 vmap_area 结构体每条记录的实体/proc/vmallocinfo的每一行对应内核里一个struct vmap_area。这个结构体定义在include/linux/vmalloc.h里核心字段包括struct vmap_area { unsigned long va_start; // 虚拟起始地址 unsigned long va_end; // 虚拟结束地址 struct rb_node rb_node; // 红黑树节点 struct list_head list; // 全局链表节点 union { unsigned long subtree_max_size; struct vm_struct *vm; }; unsigned long flags; // 标记位如 VM_VMAP };va_start和va_end就是你在第一列看到的地址范围。flags里的VM_VMAP、VM_IOREMAP、VM_USERMAP等对应你在输出里看到的vmap、ioremap、user标记。vm指针指向关联的vm_struct那里记录了物理页数组pages和调用者信息。3.2 红黑树 链表两套索引各司其职内核用两套数据结构管理所有vmap_area一棵红黑树按虚拟地址排序用于快速查找某个地址属于哪个区域一个双向链表按分配顺序串联用于遍历所有区域。/proc/vmallocinfo遍历的就是这个链表所以输出顺序基本是分配顺序但不绝对因为释放和重用会打乱。红黑树的存在是为了让find_vmap_area()这类查找操作达到 O(log n)。当你调用vfree()时内核需要快速找到地址对应的vmap_area红黑树就是干这个的。而链表遍历是 O(n)所以当 vmalloc 区域数量巨大时比如几万个cat /proc/vmallocinfo会明显变慢甚至卡住几秒。这一点在排查时要有心理准备。3.3 调用栈是怎么被记录下来的你可能会好奇内核怎么知道是哪个函数申请的答案在__vmalloc_node_range()里。分配时会调用__builtin_return_address(0)拿到返回地址再通过sprint_symbol()把地址翻译成函数名偏移的格式存到vmap_area-caller字段。所以这个调用栈只有一层不是完整的栈回溯。这就解释了为什么你看到的调用栈往往不够精确——它只记录了直接调用vmalloc的那个函数的返回地址。如果中间隔了好几层封装你看到的可能是最外层的封装函数。要拿到完整栈得靠 ftrace 或 crash 工具。3.4 vmalloc 区域的大小上限从哪来vmalloc 虚拟地址空间不是无限的。在 x86_64 上内核虚拟地址空间布局里vmalloc 区域通常从0xffffc90000000000开始到0xffffe8ffffffffff结束理论上有约 32TB。但实际可用远小于这个数原因有三一是vmap_area结构本身要占内存每个区域至少一个结构体二是地址对齐要求会产生空洞三是VMALLOC_START和VMALLOC_END之间的空间还要跟其他区域如 vmemmap共享。在 32 位系统上这个问题更严重vmalloc 空间可能只有几百 MB所以嵌入式开发里vmalloc失败是家常便饭。理解这个上限你才能判断到底是泄漏了还是单纯空间不够。4. 实战排查三类典型问题的完整链路理论讲完进入我最想分享的部分——实际怎么用这个文件破案。下面三个案例都是我或身边同事真实遇到过的场景我把排查链路完整还原。4.1 案例一内核模块反复加载卸载导致 vmalloc 泄漏现象某驱动模块用insmod/rmmod反复测试跑了几百次后系统报vmalloc: allocation failure: 65536 bytes新模块加载不进去。第一步看总量。执行cat /proc/vmallocinfo | wc -l发现行数从最初的几百涨到了几万。再用awk统计总大小sudo awk {sum $2} END {print sum/1024/1024 MB} /proc/vmallocinfo结果虚拟空间占用从几十 MB 涨到了几个 GB明显泄漏。第二步定位泄漏源。用awk按调用栈聚合sudo awk {print $3} /proc/vmallocinfo | sort | uniq -c | sort -rn | head -20输出里某个函数名出现了上万次正是那个驱动模块的xxx_alloc_buffer。问题锁定模块卸载时没有正确vfree。第三步验证。查看该函数的调用栈偏移结合模块的rmmod路径发现rmmod时只释放了主结构体漏掉了内部的一个缓冲区。修复后重新测试vmallocinfo行数稳定在几百问题解决。这个案例的关键经验vmallocinfo的行数比总大小更能反映泄漏因为泄漏往往是很多个小分配而不是一个大分配。行数暴涨基本就是泄漏的铁证。4.2 案例二BPF 程序占用大量 vmalloc 空间现象一台跑容器负载的机器vmallocinfo显示大量bpf_prog_alloc记录虚拟空间占用接近上限。排查思路BPF 程序尤其是 eBPF在加载时会通过bpf_prog_alloc分配 vmalloc 空间存放 JIT 后的指令。如果容器频繁创建销毁、每个容器都挂 BPF 程序而程序没有及时释放就会累积。用这条命令看 BPF 相关占用sudo grep bpf_prog_alloc /proc/vmallocinfo | awk {sum $2} END {print sum/1024/1024 MB}如果这个数字持续增长说明有 BPF 程序泄漏。进一步用bpftool prog show对比实际加载的程序数量就能确认是不是程序已卸载但内存没释放。经验BPF 相关的 vmalloc 占用在容器密集场景下很容易被忽视因为meminfo看不出来只有vmallocinfo能暴露。定期监控bpf_prog_alloc的总量是个好习惯。4.3 案例三ioremap 区域过多导致虚拟地址碎片化现象某嵌入式设备驱动频繁ioremap/iounmap设备寄存器运行几天后vmalloc失败但vmallocinfo总大小并不大。排查用grep ioremap /proc/vmallocinfo | wc -l发现 ioremap 区域有几千个虽然每个都不大几 KB但把虚拟地址空间切得七零八落。vmalloc 需要连续的虚拟地址碎片化后即使总空闲空间够也找不到足够大的连续区间。解决把频繁映射的设备寄存器改成一次性ioremap长期持有避免反复映射。或者用devm_ioremap让内核自动管理生命周期。经验虚拟地址碎片化是 vmalloc 失败的隐形杀手。看vmallocinfo时不能只看总量还要看地址区间的连续性。如果空闲区间都是零散的小块那大分配必然失败。5. 把 vmallocinfo 用出花监控、对比与自动化手动cat一次只能看快照真正有价值的是持续监控和对比。这一节分享几个我常用的套路。5.1 用脚本做定时采样和差异对比写个简单的脚本每隔一段时间采样一次对比两次之间的差异就能发现谁在偷偷增长#!/bin/bash # 采样两次 vmallocinfo输出新增的分配 sudo cat /proc/vmallocinfo | awk {print $1, $2, $3} | sort /tmp/vm1.txt sleep 60 sudo cat /proc/vmallocinfo | awk {print $1, $2, $3} | sort /tmp/vm2.txt comm -13 /tmp/vm1.txt /tmp/vm2.txtcomm -13会输出只在第二个文件里出现的行也就是这 60 秒内新增的分配。如果某个调用栈反复出现那就是增长源。5.2 按调用栈聚合的统计表把vmallocinfo按调用栈聚合做成一张表能一眼看出谁是大户调用栈分配次数总虚拟大小平均大小module_alloc152348 MB32 KBbpf_prog_alloc892210 MB240 KBpcpu_create_chunk6416 MB256 KBioremap320112 MB4 KB这张表用一条awk就能生成sudo awk {count[$3]; size[$3]$2} END {for (k in count) print k, count[k], size[k]/1024/1024 MB} /proc/vmallocinfo | sort -k3 -rn看这张表时我关注两个信号次数异常多可能是小对象泄漏和总大小异常大可能是大块泄漏。两者结合调用栈名字基本能定位到模块。5.3 结合其他 /proc 文件交叉验证vmallocinfo不是孤立的跟这几个文件配合看效果更好/proc/meminfo里的VmallocTotal、VmallocUsed、VmallocChunk给出总量视角。VmallocChunk是最大的连续空闲块如果它很小说明碎片化严重。/proc/slabinfo看 slab 占用排除是 slab 泄漏而非 vmalloc 泄漏。/proc/modules看已加载模块跟module_alloc的记录对照。我一般的顺序是先看meminfo的VmallocUsed是否异常异常再进vmallocinfo定位最后用slabinfo排除干扰。5.4 内核启动参数对 vmalloc 空间的影响在 32 位系统或内存受限环境可以通过vmalloc启动参数调整 vmalloc 区域大小。比如vmalloc512M会把 vmalloc 空间设为 512MB。这个参数在vmallocinfo里看不出来但会影响你能看到的总量上限。调试时如果发现 vmalloc 空间特别小先检查启动参数。6. 那些文档不会告诉你的坑与心得最后这部分是我踩过的坑和总结的经验属于书上没有、但实际会要命的内容。6.1 读 vmallocinfo 本身可能卡住系统前面提过遍历链表是 O(n)。当 vmalloc 区域有几万个时cat /proc/vmallocinfo会持有vmap_area_lock自旋锁较长时间期间其他 vmalloc/vfree 操作全部阻塞。在生产环境上如果你在内存紧张时去cat这个文件可能直接把系统卡死几秒甚至触发 soft lockup。建议生产环境采样时加timeout并且尽量在低峰期做。或者用perf之类的工具间接观察避免直接读这个文件。6.2 调用栈信息可能误导你前面说过调用栈只有一层。更坑的是如果分配发生在中断上下文或某些特殊路径__builtin_return_address拿到的地址可能不准显示成?或者错误的函数。遇到这种情况别死磕调用栈改用ftrace跟踪__vmalloc_node_range的调用者。6.3 虚拟大小不等于物理占用新手最容易犯的错看到vmallocinfo总大小几个 GB就以为物理内存被吃了几个 GB。实际上 vmalloc 的虚拟大小往往远大于物理占用因为对齐、guard page、以及分配了但没实际触碰的页都算在虚拟大小里。判断物理占用要看pages列的总和或者直接看meminfo。6.4 释放后区域可能被缓存而非立即归还vfree()之后虚拟地址区域不一定立即从vmallocinfo消失。内核有延迟释放机制vmap_area的 lazy free尤其是在CONFIG_MMU且开启了相关优化时。所以如果你vfree后马上cat可能还看到残留记录。等几秒或触发一次内存回收再看。6.5 不同内核版本的输出格式有差异老内核如 3.x的vmallocinfo输出格式跟 5.x、6.x 不完全一样标记位和列的顺序可能有变化。跨版本对比时要注意。比如早期版本没有pages列只有大小和调用栈。写自动化脚本时最好先探测内核版本再决定解析逻辑。6.6 一个实用的小技巧用 vmallocinfo 反查模块如果你怀疑某个模块泄漏但不知道具体函数可以这样反查先lsmod拿到模块的地址范围然后在vmallocinfo里找落在该范围内的记录。模块的module_alloc区域地址通常跟模块加载地址相关对照着看能快速缩小范围。# 查看模块地址 cat /proc/modules | grep 模块名 # 在 vmallocinfo 里找相近地址 sudo grep -i 0xffffc900 /proc/vmallocinfo | head这个技巧在排查第三方闭源驱动时特别有用因为你拿不到源码只能靠地址反推。6.7 关于 vmallocinfo 的权限这个文件默认只有 root 能读普通用户cat会报Permission denied。这是有意的因为调用栈信息可能泄露内核内部结构。做监控时记得用 root 或加 sudo容器环境里要注意CAP_SYS_ADMIN权限。7. 从 vmallocinfo 延伸出去还能看什么vmallocinfo只是内核内存观测的一个切面。如果你对这个方向感兴趣可以顺着往下挖几个相关接口/proc/vmallocinfo看虚拟分配/sys/kernel/debug/vmalloc在开启 debugfs 后能看到更详细的信息/proc/kallsyms配合调用栈偏移能反查具体函数。另外crash工具的vm命令能在内核崩溃后分析 vmalloc 区域跟vmallocinfo是互补的。我在实际使用中的体会是vmallocinfo最大的价值不在于看总量而在于定位到具体调用栈。总量异常只是报警真正解决问题靠的是那一列函数名。所以每次排查我都会把重心放在按调用栈聚合上而不是盯着总大小发呆。另外养成定期采样对比的习惯比出问题后再去cat一次要有效得多——很多泄漏是渐进的等你发现时已经晚了。
RELATED READING

延伸阅读

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