写这篇之前先交代一个背景我做QNX相关的开发调试有几年了平时排查问题用得最多的三个命令就是pidin、pmap和hogs其中pmap又是分析内存问题时第一个要抓的工具。很多人一开始上手QNX觉得pmap输出乱七八糟看不懂其实它把每个进程的虚拟地址空间和物理内存占用关系列得清清楚楚。这篇文章就拿pmap展开聊一聊讲它的输出怎么读、能帮我们发现什么信息、以及实际排查内存问题时怎么用它定位中间也会穿插一些我踩过的坑和总结出来的习惯。1. QNX内存机制入门看不懂pmap是因为底层概念没打通1.1 虚拟地址和物理地址先分清“看得见”和“用得上”在Linux上大家可能习惯用top、free这类工具看内存但在QNX里pmap的角度完全不同。QNX是一个微内核实时操作系统进程之间通过消息传递IPC通信内存管理上延续了经典的“虚拟地址空间物理页帧”模型。pmap做的事情是把某个进程的虚拟地址段映射关系、关联的物理内存量、映射属于什么对象这三类信息摊开来给你看。要读懂pmap建议先把三个概念在脑子里面立起来虚拟地址只是进程地址空间里的一个区间编号物理内存才是真正占用的RAM。pmap输出的很多列第一块是在标注虚拟地址的分布后面标的是这个虚拟区间对应的物理内存是不是真的驻留resident、驻留了多少。QNX里内存一定是按页管理的默认页大小通常是4K可配置为64K大页所以pmap里看到的地址范围和字节数本质上都是页的整数倍理解这一点后面读那些“奇奇怪怪”的数值就不会发怵。QNX的虚拟地址空间布局和Linux不太一样比如0地址附近通常有lowmem区内核映射区、进程自身的堆栈也有固定的地址范围。pmap本身的输出里你看到的很多“Mapped Object”列标注为[ anon ]或者[ phys ]含义差很多。[ anon ]表示匿名内存纯粹是进程自己分配的读写内存不关联文件[ phys ]表示物理内存直接映射区常见于设备寄存器映射、共享内存、或者某些系统库的固定映射如果看到/proc/boot/...这类路径那就说明这个区间映射的是一个可执行文件或共享库的某个段。搞清楚这四个来源你才能知道一个进程到底“吃”在哪。1.2 RSS、VSZ和busy三个数管住内存看板的三个视角pmap的头几行列的是进程范围内的汇总VSZ是整个进程虚拟地址空间大小RSS是实际驻留的物理内存大小。虚拟空间大不代表真的占内存比如一个进程malloc了1GB但只碰了其中几页VSZ很大RSS很小。反过来如果RSS接近VSZ那基本说明所有映射页都被实打实碰过了。实际项目里很多“内存一直涨最后OOM”的问题用这两个数就能做初步判断VSZ狂涨但RSS不大优先怀疑是虚拟地址空间黑洞比如不停地创建线程栈但没释放RSS狂涨就优先怀疑物理页确实在被消耗。busy这个数很容易被忽略但实际排查泄漏时它的作用比RSS还大。QNX系统的物理内存管理里有一个“busy page”的概念指的是当前被某个进程或内核占住、不可回收的物理页。pmap里Busy列统计的是进程所有映射里busy page的总数而空闲物理内存的计算也得看系统整体还剩下多少non-busy页。简单说RSS告诉你进程认为自己在用多少物理页busy告诉系统这些页实际上已经锁定了多少。如果进程的RSS不高但系统free内存一直少那很可能是别的机制在搞事busy能帮你从进程维度找到元凶。1.3 一台车的比喻帮你串起来我一直喜欢拿一个比喻讲这三个概念虚拟地址空间就像一张城区地图上面画满了楼盘映射段RSS相当于楼盘里真正有人住的户数busy则是那些已经上了锁、谁都不能动的房间数。QNX看进程内存要同时看地图范围、实际入住数、锁定数才全面。只看地图可能觉得一片繁荣实际住的人很少只看已入住可能漏掉虚地址耗尽的隐患只看锁定的房间又会忽略正常使用的部分。pmap的输出就是把地图上每一栋楼的具体情况给你标记出来了接下来就是你自己的判断力了。2. pmap核心输出逐列拆解那些数字到底在说什么2.1 从一条典型输出看字段含义实际在QNX终端敲pmap pid输出大概长这样pmap 410005你会先看到一行进程汇总结然后是一长串映射条目。每条映射大致是Address Bytes RSS Mode Mapped Object 08048000 1052672 1040384 r-x /proc/boot/procnto 080b0000 4096 4096 r-- /proc/boot/procnto 08600000 4194304 1048576 rw- [ anon ] 68d00000 104857600 100663296 rw- [ shmem ]这些列的读法是Address表示这段映射在进程虚拟地址空间里的起始地址Bytes表示这段映射的总长度单位是字节RSS是这段映射里实际驻留物理内存的字节数Mode是权限r表示可读、w表示可写、x表示可执行还有s表示共享等最后一列Mapped Object是映射来源。看懂RSS和Bytes的区别是核心。Bytes是虚拟区间多大RSS是这个区间用了多少物理页。两者相差很多说明这段映射虽然保留了大地址但访问过的页很少典型的例子是malloc大块内存但只碰了一角。反过来RSS接近Bytes说明这段区间被用透了比如线程栈、密集读写的数据区你看到它占了多少虚拟物理也就占了多少。2.2 Mapped Object四种来源的识别心法把映射对象分清楚基本就能判断出内存的去向。我日常把它分成四类第一类是可执行文件和共享库能直接看到路径如/proc/boot/libc.so.4。这类映射通常有多个分别对应代码段r-x、只读数据r--、数据段rw-。如果你看到某个共享库的RSS异常高通常不是库本身泄漏而是进程把库里的全局数据用得很狠或者调用了某些操作导致内部缓存膨胀。第二类是匿名内存[ anon ]没有文件背书是进程自己向系统要的页。堆、线程栈、malloc等基本都在这里。进程内存发疯式增长八成是这一类里面的某个区间在涨。第三类是[ shmem ]共享内存。QNX的IPC里使用共享内存做大数据传输时很常见多个进程可以映射同一段物理内存。用pmap看单个进程时看到的RSS只是它自己的驻留页注意共享同一物理内存的进程彼此时RSS都会计入累加会重复计算物理消耗。第四类是[ phys ]一般出现在驱动、BSP或者直接访问物理地址的场景。这个区间通常不加缓存uncached或者配置了特殊属性实际项目里如果看到某段[ phys ]的RSS很大往往对应设备DMA缓冲、寄存器映射等排查时需要结合硬件设计看。2.3 输出顺序和单位的心智模型pmap默认的输出顺序是从低地址到高地址而不是按RSS排大小。遇到一个线程多、映射段多的进程输出可能上百行这时候别急着肉眼看——把输出重定向到文件用awk、grep做二次处理更高效。比如pmap 410005 | sort -k3 -n -r | head -20按RSS倒序排序就能快速找到物理内存占用最大的几个映射段这是定位“谁占大头”的第一步。另外pmap输出的字节数超过一定量时后面的数字不管多大都是字节单位不要误以为是K或者M。我习惯在命令后面加上脱机处理写个小脚本把字节转成MB显示可读性高不少。QNX自己的pmap没有直接输出MB的选项但用awk做一次/1024/1024的换算非常方便。3. 实战从pmap命令出发解析单线程与共享内存场景3.1 先带着线程视角去看pmap平时查问题尤其在多线程进程里追踪某一线程的栈或者消息传递缓冲区pmap的输出本身是按地址段组织的不会直接把“哪个线程的栈”标出来。但它仍然能配合其他工具完成这个任务。QNX里查线程通常用pidin info pid先拿到线程列表和各自ID然后用pidin sysmgr或者直接进proc服务去看线程的tls、栈地址信息。一般线程栈都落在进程虚拟地址空间的匿名内存区域。比如你要确认线程410005的栈到底多大、实际用了多少页可以这样操作pidin info 410005拿到线程号和各自的栈起始虚拟地址后再对照pmap输出里对应的[ anon ]区间。栈的映射段通常会被标记为rw-权限而且RSS往往接近Bytes整个栈底部被访问过。如果你的线程栈增长异常pmap里对应区间的Bytes会变大RSS可能也会跟着变大这就解释了很多栈溢出类问题为什么表现为“内存涨了但代码逻辑没变”。有一个经验QNX默认线程栈大小是64K在你没有显式设置SIGSTKSZ之类的参数时一个进程开了几百个线程光线程栈的虚拟空间就有几十兆。如果这些线程真正用到的栈不足1KRSS不会高但虚拟空间占用看起来吓人。遇到业务进程虚拟空间暴涨但物理内存没怎么变的情况先数数线程数这是最常见的解释之一。3.2 共享内存shmem如何用pmap观察QNX的IPC里有专门的共享内存接口比如shm_open配合mmap或者直接用mmap加MAP_SHARED属性。这类内存一旦映射物理页会在多个进程间共享。pmap看单个进程时看到的RSS仅仅反映“这个进程自己的映射区间占了多少页”。两个进程映射同一块1MB共享内存分别用pmap看这两个进程各自都会显示有1MB的RSS但实际上物理内存只消耗了1MB。这个特性如果没意识到累加所有进程RSS就会把共享内存重复计算得出的“总内存占用”比实际偏大很多。怎么准确判断共享内存有多大首先看Mapping Object是不是[ shmem ]其次看这行的Bytes。QNX对共享内存对象有引用计数系统层面可以通过pidin syspage或者pidin mem观察物理内存整体使用情况但进程维度的“重复计算”问题始终要心里有数。排查内存压力时如果RSS总量接近物理内存上限而free内存还很多大概率就是共享内存或page cache之类的重复计了。3.3 pmap不只能看进程也能帮助定位IPC缓冲区IPC场景里最常见的一个隐藏内存消耗就是消息传递时的临时内存拷贝和mempool缓冲。QNX的IPC机制中发送方和接收方在消息复制时可能使用内核buffer这些buffer不一定在进程的pmap里显式体现它会体现在内核内存统计里需要结合pidin kernel看。pmap能看到的是进程自己为了IPC准备的数据缓冲比如用msg_sendv带大缓冲或使用共享内存传大数据。所以我的习惯是这样当怀疑IPC导致内存问题时先pmap看这个进程的匿名内存和共享内存段有没有异常再pidin kernel看内核内存总量最后通过pidin mem看系统物理内存分布。三个信息拼起来才能找到“数据在哪个环节被复制了一份”。单独用pmap容易把IPC带来的内存问题归错到用户态逻辑上。4. QNX内存泄漏排查实录pmap的完整使用流程4.1 从“内存缓慢增长”到定位可疑模块有一次我们现场反馈一个常驻服务进程运行三天后内存占用持续上升直到系统内存不足。按照我的固定流程第一步先用pidin mem看系统物理内存分布确认整体free在下降排除page cache等正常波动。第二步用pmap pid看目标进程主要看两个汇总指标VSZ和RSS同时记录进程线程数。第一步打出来的输出里RSS从启动初的30MB涨到了两天后的260MBVSZ也是同步上升的这说明不是虚拟地址黑洞而是真有物理页在被消耗。接着连续采样两次间隔十分钟对比各映射段的RSS变化重点看[ anon ]区间的增长量。实测下来某段起始地址2a000000、大小约128MB的匿名内存区段在十分钟内从40MB涨到了48MB其它段基本稳定。基本可以断定泄漏源在这个匿名区段对应的用户态内存分配里不涉及文件映射和共享内存。再往后配合malloc调试工具比如在代码里加入内存统计最终定位到是一个循环里不断把消息加入队列却忘记释放和pmap观察的阶段完美吻合。4.2 busy参数配合使用绕过RSS盲区还有一类场景RSS看着不涨但系统内存还是少了。这时候看busy就特别管用。某个驱动进程在pmap里RSS一直维持在80MB但系统free内存持续下降后来我们查pmap输出里各段的Busy列发现一个[ phys ]区间的busy数量异常高。驱动把DMA描述符池映射到了用户态但因为cache一致性处理不当导致每次操作都把页锁定并缓存了物理页无法释放。RSS没有体现出来busy却暴露了问题。注意这里说的busy不是pmap每行都必有的独立列而是在某些输出模式下会有Busy或者可以借助pmap -B之类的模式看到更详细的物理页状态。实际工程中如果盯物理内存消耗busy比RSS更接近“真实占用”的语义尤其设备驱动和DMA场景。判断busy是否合理一般和物理页总数对照系统中busy总和超过物理内存的30%、40%就要警醒了。4.3 用脚本把排查过程固化下来手动敲pmap一次、两次还行长时间追踪就得上脚本。我通常这样写while true; do date pmap_trace.log pmap pid | grep \[ anon \] pmap_trace.log sleep 300 done然后定时拉数据看匿名内存段的增长趋势几分钟一行基本能把“哪个时段开始涨”对应到业务变化上。再进阶一点用pidin mem同时记录系统内存快照这样进程维度和系统维度都能对上定位问题的时间可以缩短一半。这个脚本不需要太复杂关键是输出字段要稳定方便diff和排序。pmap输出格式有细微调整时脚本里的grep模式也要跟着调整这是实际操作中最容易踩的坑——我用过字段名缩写导致脚本在QNX7.1和8.0之间失效的情况后来统一在脚本里写了格式解析函数兼容两套输出。5. 常见问题与排查技巧实录5.1 pmap输出里最常见的误判头号误区是把VSZ当物理内存看。虚拟空间大不是真占用尤其QNX里进程的地址空间上限很大一个进程的VSZ能轻松到GB级别不代表它占了GB物理内存。判断一个进程是否吃内存先看RSS和busy再看VSZ。第二个误区是按RSS排序人肉比对。pmap输出默认低地址到高地址排列不是按RSS排序眼睛扫很容易漏掉真正的“大头”。用sort处理一次结果完全不同。第三个误区是忽略了共享库的RSS。很多共享库的映射段RSS看起来不小比如libc的bss段但它是被所有进程共享的物理页。单个进程pmap里显示几百KB一百个进程加起来还是那几百KB不是乘以进程数。5.2 快速判定是泄漏还是正常业务缓存有一个很实用的土办法连续两次pmap采样的差值就是新增量。如果新增量长期稳定增长且每次增量和使用频率强相关有可能是正常缓存如果增量在业务空闲时依然保持那就基本是泄漏。判断时最好同时看所有映射段的增长不只看[ anon ]。有些泄漏会体现在文件映射段比如mmap了临时文件忘了munmap那种情况RSS和Bytes都会涨但Mapped Object带路径排查方向就不同了。处理大型服务建议采集时长超过一个业务周期比如请求高峰叠加请求低谷避免只看到某一段增长就下结论。我们遇到过因为日志量大导致日志缓冲正常扩张被误判为内存泄漏的情况连续采样看到低谷期能回落才纠正回来。5.3 几个问得特别多的问题速查问pmap显示某个共享库RSS特别大是泄漏吗先看这个库被多少进程共享再用pidin mem确认物理页是否真的被占用。库的全局缓存、锁池等会增加RSS但不一定是泄漏需要结合这个库的功能判断。问匿名内存段RSS大但代码里看不到明显大数组怎么办先看段大小是固定还是持续增长。如果固定可能是一次性分配的池比如线程池预留的栈空间如果增长优先排查每线程或每次请求内的动态分配。可以配合QNX自带的malloc调试功能设置MALLOC_DEBUG之类的环境变量跟踪堆分配轨迹。问pmap能不能直接看到某个线程的栈大小pmap不能直接给线程栈标名但结合pidin info pid拿线程栈地址再回pmap里定位对应的匿名段完全可行。要是栈区间没有独立映射就会藏在堆附近的匿名段里这时要用pstack或者pidin stack辅助确认。问为什么我看到的pmap列和网上其他人不一样QNX的pmap在较新版本比如7.1、8.0输出有过调整增加了物理地址列、busy列有的版本还支持-a参数打印更多属性比如cache属性。遇到输出不一致先确认版本再查对应版本文档别拿旧经验硬套。几点补充经验沉下心把pmap玩熟之后你会发现QNX的内存分析其实是一个“多层次对照”的过程。pmap帮你从进程维度看映射和驻留pidin mem帮你从系统维度看物理内存分配pidin kernel帮你查内核缓冲hogs能辅助看CPU和内存的关联。遇到疑难内存问题别只靠一个命令多维度数据拼图才是最快的路径。我个人的操作习惯是每次新接手一个QNX项目先把所有关键进程的pmap基线快照存一份版本更新或需求变更后再做对比。基线加对比能省掉大量“这内存到底正不正常”的争论时间。还有一个小技巧——在pmap输出里看到[ anon ]段数量特别多、每段都只有几KB时大概率是线程栈或小的临时分配池碎片这类场景可以结合pidin threads看看线程数数一下对应关系基本就能确认。最后再说一句pmap不是从天上掉下来的工具它基于的系统内存模型完全可以类比到别的RTOS和嵌入式Linux上核心都是“虚拟映射、物理页、驻留、锁定”这几个维度。把pmap读透换到别的嵌入式平台时你对内存分析的思路也是通用的。