ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

QNX内存分析利器pmap:地址空间映射与内存泄漏排查实战

QNX内存分析利器pmap:地址空间映射与内存泄漏排查实战 1. 为什么QNX上要做内存分析pmap能帮我们看到什么做QNX开发的朋友应该都有这种体会这系统跑起来稳是真稳但一旦要查内存问题就感觉手头的工具比Linux上少了一大截。Linux下你有top、free、ps aux、/proc/$pid/smaps一套组合拳下来基本能把内存看个底朝天。到了QNX这边能用顺手的就是pidin那几条命令剩下就剩下论坛里翻帖子了。这个项目标题挂的是pmap一听就是从Linux那边带过来的习惯。实际上QNX确实提供了pmap工具在SDP里自带功能上就是查看某个进程的地址空间映射关系。但坦白说QNX的pmap输出格式和Linux不太一样字段含义也有差异刚上手的时候很容易看懵。再加上QNX本身是微内核架构进程之间大量使用共享内存和消息传递内存的归属问题比Linux要复杂得多——你以为某个库占的内存是在A进程里实际上它在B进程里被map了一份结果两边都显示占着。所以这篇东西我想花点篇幅把pmap到底能看什么、不能看什么、怎么用、怎么和pidin配合起来排查内存问题一次性说清楚。适合谁看刚接触QNX没两年的应用层开发者以及在QNX上做系统调优、做内存泄漏排查的嵌入式工程师。Linux背景的人看了收获最大因为很多思路能迁移过来但细节一定要重新建立。2. 核心思路拆解QNX的内存模型和pmap的定位2.1 QNX内存管理的基本单位region要理解pmap得先从QNX的进程内存模型讲起。QNX不像Linux那样用页表和VMA虚拟内存区域体系它有一套自己的进程管理器proc用region来描述进程地址空间。一个region是一段连续的虚拟地址范围关联了一个对象object以及一组权限属性。这个object可以是文件映射、共享内存、匿名内存、物理内存直接映射甚至是内核对象。每当你调用shm_open建立一个共享内存、用mmap映射一个文件、或者进程启动时加载一个.so库proc都会给这个进程的地址空间里增加一个或多个region。pmap做的事就是遍历某个进程的所有region把地址范围、权限、关联对象名称、映射类型列出来给你看。它就是proc在你面前开的扇窗。这里有个关键点pmap侧重的是虚拟地址空间的映射视图而不是物理内存的实际消耗。也就是说它告诉你这个进程“能看到哪些内存”但某个region的页是否真的分配了物理页框看pmap是不完全的。它有一个字段能反映一部分但不像Linux smaps那样有RSS/PSS这种细粒度统计。这一点心里要清楚不然很容易得出错误结论。2.2 为什么需要pmapQNX上内存分析的一大痛点QNX系统上排查内存问题最头疼的场景有这么几类第一内存越界导致某个进程崩溃。你怀疑是访问了非法地址但不知道这个地址在进程地址空间里属于哪段。用pmap把进程的region列表捞出来就能判断那个地址落在哪个region里是heap是某个.so的text段还是压根是野指针指向了空洞第二内存泄漏查不到源头。进程的内存稳步上涨sspset server page没有明显异常pidin info能告诉我们进程的总内存数在涨但没法告诉我们涨在哪里。这时候pmap就能看哪些region大小在膨胀、哪些region数量在增加。比如某个.so反复被加载或者某个共享内存在不停新建而没有销毁pmap里会看得比较清楚。第三多进程共享内存归属不清。QNX的进程之间大量用共享内存做数据传递共享内存本身是系统级的资源不归属于任何一个进程。pidin看某个进程的内存占用是能看出来的但你想知道“这块共享内存到底被哪几个进程引用”的时候pmap反而比pidin更好使因为它会显示region引用到的共享内存对象名。所以我个人习惯这样搭配内存占用宏观趋势用pidin地址空间微观结构用pmap两者配合才能把内存问题定位到根上。2.3 和Linux pmap的差别别把习惯直接搬过来写这节不是抬杠是真的建议Linux背景的兄弟注意几个差异点。Linux的pmap输出有Virtual Memory Size、RSS、Dirty这些列能直接告诉你这个映射实际占了多少物理内存。QNX的pmap不给你这些统计它更关注mapping的属性和权限。我看过好几个人上来就问“为什么pmap没有RSS列”其实不是没有而是QNX的哲学不一样它默认你是在实时环境里调试更有价值的通常是权限和对象关系而不是页级统计。另外Linux的pmap参数-x、-d、-q在QNX上并不通用。QNX的pmap主要有-p指定进程ID、-a显示所有进程、-s显示共享内存摘要、-v做个详细输出。功能上够用但你要重新记参数不能拿Linux肌肉记忆去敲。这个差别背后是两套系统设计目标的不同。Linux追求通用计算场景下的可观测性和灵活性QNX在实时嵌入式环境里更追求直接、高效、可预测。工具设计也跟着系统哲学走理解了这层再去看那些字段就顺了。3. pmap输出逐字段拆解每个数字和名字代表什么3.1 先跑起来看一个实际输出样例我拿一个典型的QNX进程举例假设这是一个叫camera_app的摄像头应用PID是8842。在shell里执行pmap -p 8842输出大致长这样我精简了部分行PID 8842: camera_app virt 120540K res 34820K Addr Len Region ID Perms Object 08048000 001000 134 r-x /bin/camera_app 080bb000 004000 135 rw- /bin/camera_app 49f2a000 001000 802 r-- /lib/libc.so.5 49f2c000 04b000 803 r-x /lib/libc.so.5 49f78000 007000 804 rw- /lib/libc.so.5 4a0aa000 000100 810 r-- /proc/8842/asinfo 4a100000 0400000 850 rw- anonymous ...第一行是进程级别的汇总virt是虚拟内存总量res是驻留内存量单位是KB。这个res其实就是pmap和pidin info口径比较接近的一个数也是判断进程内存占用最直接的抓手。然后从第二行开始每一行就是一个region字段从左到右分别是起始地址十六进制、长度KB、region ID、权限、对象名。整个视图就是进程地址空间的“切片展览”。3.2 Addr和Len地址范围决定“这是哪块地”Addr是region的虚拟起始地址Len是长度。这两个值合起来就是一个虚拟地址区间。调试崩溃堆栈时最常用的就是拿崩溃地址去比对。举个例子假如camera_app在0x49f64400处崩溃你拿这个地址和pmap输出对上发现它落在/lib/libc.so.5的r-x段里那就是在C库代码里崩了八成是你传了非法参数给某个libc函数跟业务代码关系不大。如果落在anonymous区域那就是堆上出了问题需要优先查malloc/free的逻辑。还有一点值得注意Region ID在追踪region生命周期的时候很有用。如果一个进程反复创建和销毁共享内存你抓两次pmap输出看Region ID序列的变化速度就能侧面判断是不是有创建后忘了解除映射的问题。3.3 Perms权限位不光是读写权限还是类型线索Perms这一列看起来只是r-x、rw-这些但实际信息量很大。r-x通常映射的是可执行文件或共享库的代码段。正常情况下进程里r-x的总量基本不变如果它异常增长大概率是动态加载了不该加载的东西。rw-可读可写一般是数据段、堆、匿名映射。这个是重点盯防对象内存泄漏和堆膨胀都体现在这。r--只读段比如rodata、某些只读映射。r-x和r--混合的部分一个.so可能在pmap里显示多段分别对应text、rodata、data这是正常的段拆分。还有一个特殊权限位组合值得注意r-x后面跟着共享对象的通常意味着这块是从文件映射的共享只读代码所有进程共享物理页框。如果一个进程里r-x区域特别多不一定是内存泄漏可能只是它加载了特别多的.so。这时候要去查是不是链接了冗余的动态库。3.4 Object列名字暴露一切Object列是pmap输出里最高价值的一列它直接告诉你这个region的“后台老板”是谁。可执行文件名如/bin/camera_app进程自身的代码和数据段所有进程都有不值得奇怪。共享库路径说明这个进程链接了此库。排查内存的时候可以通过统计这些库region的总长度判断哪些库占地方大。anonymous无名的匿名映射通常是堆、栈、匿名mmap。这个最常见也是内存泄漏重灾区。/dev/shmem/*或者自定义共享内存对象名说明这个region映射的是系统共享内存。这里要注意如果出现在多个进程里说明这些进程在共享同一块物理内存那这块内存就不该简单记到任何一个进程头上。/proc/8842/asinfo这种是procfs相关的映射一般是系统内部用的不用管它。我处理过一个案例某个服务进程RSS看着不大但系统总内存却很紧张。最后用pmap全量扫了一遍所有进程发现有个共享内存对象被十几个进程同时映射了每个进程看自己都只占一点点但累积起来是很可观的一块物理内存。这就是Object列的价值——它能帮你把零散进程之间的共性揪出来。3.5 pmap -s共享内存视角的补充pmap还有一个子功能pmap -s是按共享内存维度来展示。它会列出系统里所有命名的共享内存对象、大小、以及引用计数。这个在排查多进程共享内存泄漏时非常好用。举个例子pmap -s | grep -i conf能看到/conf_shared这种共享对象在被哪些PID引用、映射了多少空间。如果你发现某个共享对象始终存在、引用计数却不归零那基本可以断定某个进程忘了munmap或者shm_unlink。4. 实操从0开始的一次完整pmap内存排查4.1 第一步锁住目标进程拿到PID列表一般排查内存问题起点不会是“我要用pmap”而是“我感觉系统内存不对了”。所以先要从宏观上锁定嫌疑进程。在QNX shell里最粗的筛法是用pidinpidin mem能看到全系统物理内存的使用总量、空闲量判断是不是整体缺内存。如果整体紧张继续用pidin infopidin info这个会列出所有进程其中有一列是内存驻留大小。找到增长最快、占比最大的进程记下它的PID。补充一个小技巧pidin info的进程列表默认不是按内存排序的如果进程很多可以直接用命令加管道排序pidin info | sort -k5 -n -r | head -20这里的第5列是RSS大小排序出来前20个直接就是内存大户。4.2 第二步用pmap看目标进程的地址空间布局锁定了PID比如是PID 8842就执行pmap -p 8842第一件事看第一行的virt和res差值。如果出来一个virt远远大于res说明这个进程虚拟空间很大但实际驻留不高一般问题不大可能是映射了大的文件但没实际读取多少。反过来如果res接近virt说明映射的页基本都实打实占物理内存了就要小心了。第二件事数一下region的行数。正常一个QNX进程的region在几十到上百行之间如果上千行且有大量重复名称的region基本可以断定这里有反复映射释放不全的问题。第三件事是盯anonymous区域的大小。可以用命令求和pmap -p 8842 | awk /anonymous/ {sum strtonum($2)} END {print sum}awk的写法不同QNX版本对strtonum的支持不一样如果awk不支持就直接肉眼估算或者把输出重定向到文件里用脚本离线分析。匿名区域暴涨是堆泄漏最直接的证据。4.3 第三步前后对比抓“活点”内存问题最怕的是抓拍。一次pmap只能看到当前状态无法判断哪些region是“活”的、哪些是稳定的。所以更实用的做法是隔一段时间跑一次把结果存下来做对比。我一般是这样操作的pmap -p 8842 /tmp/pmap_1.txt sleep 300 pmap -p 8842 /tmp/pmap_2.txt diff /tmp/pmap_1.txt /tmp/pmap_2.txt看diff的结果重点看两类变化同一地址范围的Len变大说明某个region在持续扩张典型的是堆不断向上长这就是泄漏的曲线。新增了region说明进程在反复做mmap、打开新文件映射或分配共享内存如果每次diff都有新region且数量持续增加那多半是映射泄漏。这个方法比单纯看RSS增量更有价值因为它能告诉你内存到底涨在了哪里。4.4 第四步结合其他工具交叉验证pmap不是万能的。它告诉你虚拟映射的结构但物理内存是否被换出、共享页的真实驻留数它说得不够细。这个时候可以搭配几个命令交叉验证。pidin m看所有进程的物理内存驻留信息。pidin info -m针对单个进程看内存汇总。pidin as查看进程的地址空间统计信息包括栈、堆大小。pidin signal如果怀疑某个进程异常可以配合看信号状态。还有一个值得养成习惯的动作把pmap的输出保存后用脚本统计每个object的region数量和总长度写一个小工具自动化。比如统计该进程所有.so占的总虚拟内存、匿名区域占的总内存按占用排序。这个在长时间跑的系统上做定期巡检非常有用我可以直接说我踩过几次坑之后最终都是靠这招找到泄漏源头的。5. 常见问题与排查技巧实录5.1 pmap显示的res和pidin info显示的内存为什么对不上这个问题几乎每个用pmap的人都会遇到。pmap第一行的res是这个进程的驻留总量包括共享页但它不是严格去重后的物理内存占用。如果一个物理页被多个进程共享pmap会分别算到每个进程头上加起来就会超过系统总物理内存。而pidin info的RSS口径也不完全一样它倾向于算进程“私有该进程独占的共享部分”所以两者数值有差别是正常的不需要纠结。判断单个进程是否泄漏时盯同一工具的趋势别在两种口径之间横跳。5.2 pmap输出里有大量相同object的region正常吗要分情况。比如一个.so被映射了两次一次是代码段、一次是数据段这是正常的。如果同一个.so在你进程里有二三十个region就不对劲了。我遇到过的情况是某个驱动库在每次调用时都用dlopen打开又只dlclose了一部分导致代码段反复映射。判断方法pmap -p PID | grep 某个库 | wc -l如果行数异常多去代码里找dlopen的地方确认是不是有路径分支没有走dlclose。5.3 进程RSS稳定上升但pmap看不到明显泄漏怎么办这种情况通常意味着泄漏的不是进程内region而是系统级共享内存或者内核资源。进程可能在反复创建共享内存对象但没销毁它的私有望远镜里看不出异常但系统总内存却一直在少。这时候要用pmap -s看系统共享对象列表找出那些创建次数多而引用计数不为0的对象。配合系统日志里的shm创建记录基本能锁定谁在漏。另一个隐蔽场景是线程栈退避空间。有些QNX系统默认会给线程栈映射较大虚拟空间线程创建多了以后虚拟地址看着很恐怖但实际驻留不大。pmap里栈区域通常以anonymous方式展示且权限为rw-。如果发现大量地址相邻、大小接近的anonymous区域且数量持续增加多半是线程没有正确join或销毁。5.4 pmap提示permission denied权限不够是常见的坑。QNX对进程信息的访问有权限控制非root用户跑pmap -p可能被拒。解决办法是用root权限执行或者确保当前用户有PROC_PIDINFO权限。在QNX安全策略较严格的系统上还需要配置相应的权限令牌这个在开发板上一般不会遇到但在量产固件上很容易碰上。处理方式是su - root pmap -p 8842如果还是不行检查系统是否启用了ASLR和地址空间随机化这会导致进程地址空间每次启动都不一样调试时对比两次输出可能感觉对不上。这种情况关闭ASLR再定位会省很多事在QNX启动脚本里去掉相关随机化配置即可。5.5 崩溃地址和pmap对不上有一种比较常见的情况崩溃时看到的地址是个很小的地址比如0x10或者0x1C。这种不是正常的region映射地址而是空指针加偏移量访问。举个典型场景某个结构体指针为NULL代码访问了它的成员ptr-field因为field在结构体偏移0x1C处所以崩溃地址就是0x1C。这种pmap查不出什么因为还没走到非法映射那一步纯属空指针解引用。遇到这种别在pmap上浪费时间直接去查调用链里哪个指针没判空。还有一种是地址能对上region但那个region权限是r-x你却往里写数据会导致段错误。这种pmap就非常有用了一眼就能看出来是往代码段写入了几乎可以肯定是数组越界或者函数指针被破坏。6. pmap在真实项目里的三种典型实战用法6.1 用法一启动阶段抓“尺寸膨胀”系统启动初期各进程加载完库、建立完通信之后内存应该稳定下来。我在很多项目里会做一个基线快照pmap -a /tmp/map_boot.txt等系统跑到业务稳定期再抓一次pmap -a /tmp/map_stable.txt两次diff看每个进程的region数量和总虚拟内存增长。如果一个进程在稳定期后还不停增加region不用怀疑就是它在不停做动态映射而没释放。这个方法我在定位某导航中间件内存膨胀时用过不止一次每次都能很快定位到具体模块。6.2 用法二长期运行时的定时巡检把pmap包装成一个轮询脚本定期抓取关键进程的内存摘要并写日志是成本最低的预警方案。脚本里可以这么写while true; do date /var/log/mem_watch.log pmap -p $(pidin info | grep my_service | awk {print $1}) /var/log/mem_watch.log sleep 60 done日志跑到内存泄漏的时候回头看趋势曲线哪段时间region数量激增、哪段时间匿名内存开始爬坡一目了然。比起事后到处找日志这种主动监控省心很多。6.3 用法三定位某个.so的归属和内存占用QNX系统里一个进程加载了一堆.so你总想知道哪个占得多。pmap配合awk就能算pmap -p 8842 | grep -v ^/ | awk {print $5, $2} | sort | uniq -c | sort -k2 -n -r这条命令本质上是按Object列做分组统计把每个对象名下的region长度汇总出来排序后就能看到哪些库最占内存。如果某个业务库占了几十MB的rw-段它的数据结构就有问题。7. 再聊点pmap之后的事和个人的实际心得很多人以为跑完pmap、看完region列表内存排查就结束了。实际上pmap只是定位的起点定位到具体模块后更深入的工作是去代码里找出是谁创建了这个映射、为什么没有释放。我自己常用的手段是配合QNX自带的事件跟踪工具比如qnx_event或者系统trace去跟踪mmap、munmap的调用链。定位到可疑的region后用trace过滤出该进程对mmap的调用序列就能找到调用堆栈。pmap负责“看现场”trace负责“看过程”两者结合才能形成闭环。还有一个心得是要习惯用系统级视角看内存别只盯一个进程。QNX里共享内存是常态单个进程的RSS很小不代表系统不紧张。有一次线上设备内存告急我一开始盯某个媒体进程死活没发现问题后来用pmap -s把系统所有共享内存对象拉出来一看才发现有个数据分发模块创建了上百个几MB的共享内存块没有一个释放。这种情况如果你只盯着单个进程的pmap可能永远找不到。所以建议做QNX内存分析的朋友把pmap当成一个探针而不是一个仪表盘。探针能帮你找到问题的精确位置但要看到全局趋势和系统全貌还是得靠pidin和共享内存视角配合。工具是死的思路是活的。最后分享一个保存输出的小习惯pmap输出直接重定向到文件是二进制安全的而且字符集简单适合直接做diff。建议每次排查都把输出按“时间戳_PID”命名存放长期积累下来就是一份完整的内存排查档案。后续再遇到类似问题翻旧档案速度比重新分析快得多。
RELATED READING

延伸阅读

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