背景
在嵌入式设备,总会出现内存不够用的情况。我就遇到一个场景,需要把某个应用塞到资源有限的 Linux 设备中。这应用进程所需要的内存已然超过了 Linux 设备所能提供的空闲内存。
在我的场景中,应用跑起来后共享内存占所有物理内存的 70% 了。与其死抠应用代码,少申请内存,还不如想办法转用其他更少资源的共享库。
所以本文并不会涉及到应用实现上如何更少的内存使用,更多是分享如何从 Linux 系统角度分析应用内存的分布和使用情况。
对我而言,则是又一次理论指导实践的过程,嗯,理论落地的感觉真好。
出于保密的需要,本文一些数据和日志等都会用无意义字符替代,但不会影响同学们阅读和理解
内存的基本知识
在 Linux 上,应用使用的内存可以分为两类,分别是 虚拟内存 和 物理内存。应用可以肆意申请内存,在不超过寻址范围的情况下,很少会有申请失败的情况,毕竟对 Linux 来说并没有分配物理内存。直至你真的要操作申请的内存了,Linux 才真切分配物理内存。关于虚拟内存和物理内存的概念本文不阐述,我只是为了说明,分析内存占用,不能看虚拟内存,必须看物理内存。
由于在 Linux 上,有一些内存是共享的,主要集中在应用加载的动态库上。就好像最常用的 C 库,应用打印个 “helloworld” 可都要链接到 C 库。每一个应用都加载一份 C 库多浪费啊,杜绝浪费,从你我做起。于是 Linux 就只会加载一次 C 库,然后映射到不同应用的虚拟地址,实现不同应用(进程)之间“共享”一份动态库。这不是完全意义上的共享,毕竟动态库里的全局变量对每个进程而言都是私有的。正因为共享动态库的存在,我们统计应用(进程)使用的物理内存时,是否包含以及如何包含共享的动态库占用的物理内存,就产生了2个新的概念:PSS 和 USS。
准确来说,跟 PSS 和 USS 相似的词汇还有 VSS 和 RSS。网上有不少资料,我找到这么一篇文章介绍非常生动:《linux中top命令 VSS,RSS,PSS,USS 四个内存字段的解读。》,不懂的同学自己看资料哈,我这里做个简单的描述:
-
VSS:Virtual Set Size
就是虚拟内存,包括进程已经申请,虽然不一定分配了物理内存,但进程访问不会触发段错误的内存。
-
RSS:Resident Set Size
就是所有的物理内存,会统计上用到的动态库的所有内存,即使这动态库多个进程共享。
-
PSS:Proportional Set Size
也是物理内存,只不过所有动态库的内存是按比例计算,例如 N 个进程链接动态库,那么每个进程只会计算占用动态库 1/N 的空间。
-
USS:Unique Set Size
也是物理内存,但不包含动态库的内存,也就是进程私有的内存。
查看进程内存使用情况
如果要查看系统内存使用情况,常用
free
命令。但在这里,我们需要精细到进程为单位的内存使用情况。补充一点,所有线程共用一个
mm_struct
实例,因此内存使用情况以进程为单位,无法再细分到线程。
那么如何查看进程的内存使用情况呢?在 PC 上我们可以使用
top
,或者
ps
,例如:
$ toptop - xx:xx:xx up xxx days, xx:xx, x users, load average: 1.31, 1.50, 2.52Tasks: xxx total, x running, xxx sleeping, xx stopped, x zombie%Cpu(s): 1.4 us, 0.4 sy, 0.0 ni, 98.1 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 stKiB Mem : xxxx total, xxxx free, xxxx used, xxxx buff/cacheKiB Swap: xxxx total, xxxx free, 0 used. xxxx avail MemPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND35370 xxxxx 20 0 xxxxxxx56cxxxxxx xxxxx S 3.9 0.3 1:38.22 xxx39464 xxxxx 20 0 xxxxx xxxxx xxxx S 1.6 0.0 0:00.64 xxx....
上面的 VIRT 就是虚拟内存使用情况,等效于 VSS;RES 是物理内存,等效于 RSS;SHR 是共享内存。
但在大多数嵌入式设备上,
top
或者
ps
都是阉割版本的,显示的内容不全,非常无奈,例如我在 openWRT 上执行
ps
,只会显示虚拟内存:
# psroot@xxxx:/# ps wPID USER VSZ STAT COMMAND1 root 3028 S /sbin/procd2 root 0 SW [kthreadd]....
除了使用第三方的
htop
之外,在一个连
ps/top
都没有的非常极端的环境,我们可以直接查询
procfs
,简单粗暴。如何做呢?
cat /proc/<pid>/statm
实际上,从
htop
的源码发现,其也是从 statm 获取的进程内存使用情况。例如,我要查看进程 1 的内存使用情况:
$ cat /proc/1/statm57 502 363 14 0 181 0
这几个数据什么意思呢?查看 Linux 的 Documentation,有如下的注释:
$ cat filesystems/proc.txt...Table 1-3: Contents of the statm files (as of 2.6.8-rc3).........................................................ad8.....................Field Contentsize total program size (pages) (same as VmSize in status)resident size of memory portions (pages) (same as VmRSS in status)shared number of pages that are shared (i.e. backed by a file, sameas RssFile+RssShmem in status)trs number of pages that are \'code\' (not including libs; broken,includes data segment)lrs number of pages of library (always 0 on 2.6)drs number of pages of data/stack (including libs; broken,includes library text)dt number of dirty pages (always 0 on 2.6)...
翻译过来就是:
第一个字段:Size : 任务虚拟地址空间的大小(单位:pages)第二个字段:Resident:应用程序正在使用的物理内存的大小(单位:pages)第三个字段:Shared: 共享页数(单位:pages)第四个字段:Trs:程序所拥有的可执行虚拟内存的大小(单位:pages)第五个字段:Lrs:被映像到任务的虚拟内存空间的库的大小(单位:pages)第六个字段:Drs:程序数据段和用户态的栈的大小(单位:pages)第七个字段:dt:脏页的数量
一般情况下,pages 的大小是 4k ,因此上述的结果除以 4 得到的值单位是 KB。在上例中:
$ cat /proc/1/statm57 502 363 14 0 181 0# 表示# 57: 虚拟内存:57/4 = 14 KB# 502:物理内存(RSS): 502/4 = 125 KB# ...
查看进程内存的分布
只是知道进程内存的分布还不能满足我场景的需求。在我的案例中,70%的物理内存都是共享内存,我还需要细致到用了什么库,每个库用了多少物理内存,我才知道该优化什么库。
要查看虚拟内存的分布情况,可以
cat /proc/<pid>/maps
,例如:
# cat /proc/1/maps00400000-0040e000 r-xp 00000000 b3:05 628 /sbin/procd0041e000-0041f000 r--p 0000e000 b3:05 628 /sbin/procd0041f000-00420000 rw-p 0000f000 b3:05 628 /sbin/procd00420000-00422000 rw-p 00000000 00:00 02e7dd000-2e853000 rw-p 00000000 00:00 0 [heap]7fa86f9000-7fa881c000 r-xp 00000000 b3:05 445 /lib/libc-2.23.so7fa881c000-7fa882b000 ---p 00123000 b3:05 445 /lib/libc-2.23.so7fa882b000-7fa882f000 r--p 00122000 b3:05 445 /lib/libc-2.23.so7fa882f000-7fa8831000 rw-p 00126000 b3:05 445 /lib/libc-2.23.s1dd1o...
但还不够,这只知道了每个库隐射到哪段虚拟内存,我还需要知道实际的物理内存。此时需要
cat /proc/<pid>/smaps
。
smaps
在我的板子上找不到,在内核中检索代码实现,发现需要在内核中使能
CONFIG_PROC_PAGE_MONITOR
。使能后重新编译内核,我们就可以获取实际的物理内存啦。例如:
# cat /proc/1/smaps...7fa8908000-7fa890e000 r-xp 00000000 b3:05 485 /lib/librt-2.23.soSize: 24 kBKernelPageSize: 4 kBMMUPageSize: 4 kBRss: 24 kBPss: 6 kBShared_Clean: 24 kBShared_Dirty: 0 kBPrivate_Clean: 0 kBPrivate_Dirty: 0 kBReferenced: 24 kBAnonymous: 0 kBAnonHugePages: 0 kBShmemPmdMapped: 0 kBShared_Hugetlb: 0 kBPrivate_Hugetlb: 0 kBSwap: 0 kBSwapPss: 0 kBLocked: 0 kBVmFlags: rd ex mr mw me...
上例子中,librt 的库有映射到虚拟内存
7fa8908000-7fa890e000
,权限是
r-xp
,虚拟内存是 24kB,RSS 是 24KB,PSS 是 6KB。
数据分析
通过以下的命令可以初步过滤 smap 的 RSS 和 PSS,再根据 PSS 排序,取 PSS 使用量最大的前20个:
cat /proc/<pid>/smaps | awk \'{if ($1 ~ /^[[:digit:]]+/)printf \"<%s> : \", $6if ($1 ~ /^Rss/)printf \"RSS = %sKB; \", $2;if ($1 ~ /^Rss/)printf \"PSS = %sKB\\n\", $2;}\' | sort -hrk 8 | head -n 20
结果如下:
...</usr/lib/libXXXXXXXXX.so> : RSS = 1544KB; PSS = 1544KB</usr/lib/libXXXXXXXX.so> : RSS = 1172KB; PSS = 1172KB</usr/lib/libXXXXXX.so> : RSS = 1148KB; PSS = 1148KB...</lib/libXXXX.so> : RSS = 1072KB; PSS = 1072KB</lib/libXXXX.so> : RSS = 1060KB; PSS = 1060KB</usr/lib/libXXXX.so.30.23.0> : RSS = 824KB; PSS = 824KB<[heap]> : RSS = 692KB; PSS = 692KB</usr/lib/libXXXX.so> : RSS = 664KB; PSS = 664KB</usr/lib/libXXXXX.so.2.0.0> : RSS = 640KB; PSS = 640KB...
由于保密的需要,我就不完整贴出来了。根据统计的结果,PPS 使用量前 20 的动态库竟然占了进程共享内存的90%的物理内存,非常夸张。
采用一些相同功能,却更小巧的动态库还是有很大效果的。此外,通过这个方法也可以知道堆和栈对内存使用的情况,在做应用内存优化的时候也可以有个侧重方向。
优化还在持续进行中,结果就不贴出来了。
总结
本文介绍了 Linux 内存的一些基本理论知识,重点还是在于如何把理论付诸实践。通过这次的应用内存裁剪的分析过程,重温了 VSS、RSS、USS、PSS,以及学习了如何在 Linux 上查询进程的内存使用情况,如何查询虚拟内存的分布,以及进程各个动态库、堆、栈的物理内存使用情况对应用内存裁剪的指导意义。
对大多数人来说,通过
top/ps
查看物理内存就到底了吧。平时注意理论积累,在关键的时候才不会像无头苍蝇。