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 为什么需要pmap:QNX上内存分析的一大痛点
QNX系统上排查内存问题,最头疼的场景有这么几类:
第一,内存越界导致某个进程崩溃。你怀疑是访问了非法地址,但不知道这个地址在进程地址空间里属于哪段。用pmap把进程的region列表捞出来,就能判断那个地址落在哪个region里,是heap?是某个.so的text段?还是压根是野指针指向了空洞?
第二,内存泄漏查不到源头。进程的内存稳步上涨,ssp(set 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里,最粗的筛法是用pidin:
pidin mem能看到全系统物理内存的使用总量、空闲量,判断是不是整体缺内存。
如果整体紧张,继续用pidin info:
pidin 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”命名存放,长期积累下来就是一份完整的内存排查档案。后续再遇到类似问题,翻旧档案速度比重新分析快得多。