Linux裁剪应用内存的一次实践
背景
在嵌入式设备,总会出现内存不够用的情况。我就遇到一个场景,需要把某个应用塞到资源有限的 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
,例如:
$ top
top - xx:xx:xx up xxx days, xx:xx, x users, load average: 1.31, 1.50, 2.52
Tasks: 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 st
KiB Mem : xxxx total, xxxx free, xxxx used, xxxx buff/cache
KiB Swap: xxxx total, xxxx free, 0 used. xxxx avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
35370 xxxxx 20 0 xxxxxxx xxxxxx xxxxx S 3.9 0.3 1:38.22 xxx
39464 xxxxx 20 0 xxxxx xxxxx xxxx S 1.6 0.0 0:00.64 xxx
....
上面的 VIRT 就是虚拟内存使用情况,等效于 VSS;RES 是物理内存,等效于 RSS;SHR 是共享内存。
但在大多数嵌入式设备上,top
或者ps
都是阉割版本的,显示的内容不全,非常无奈,例如我在 openWRT 上执行ps
,只会显示虚拟内存:
# ps
root@xxxx:/# ps w
PID USER VSZ STAT COMMAND
1 root 3028 S /sbin/procd
2 root 0 SW [kthreadd]
....
除了使用第三方的htop
之外,在一个连ps/top
都没有的非常极端的环境,我们可以直接查询 procfs
,简单粗暴。如何做呢?
cat /proc/<pid>/statm
实际上,从 htop
的源码发现,其也是从 statm 获取的进程内存使用情况。例如,我要查看进程 1 的内存使用情况:
$ cat /proc/1/statm
57 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)
..............................................................................
Field Content
size 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, same
as 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/statm
57 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/maps
00400000-0040e000 r-xp 00000000 b3:05 628 /sbin/procd
0041e000-0041f000 r--p 0000e000 b3:05 628 /sbin/procd
0041f000-00420000 rw-p 0000f000 b3:05 628 /sbin/procd
00420000-00422000 rw-p 00000000 00:00 0
2e7dd000-2e853000 rw-p 00000000 00:00 0 [heap]
7fa86f9000-7fa881c000 r-xp 00000000 b3:05 445 /lib/libc-2.23.so
7fa881c000-7fa882b000 ---p 00123000 b3:05 445 /lib/libc-2.23.so
7fa882b000-7fa882f000 r--p 00122000 b3:05 445 /lib/libc-2.23.so
7fa882f000-7fa8831000 rw-p 00126000 b3:05 445 /lib/libc-2.23.so
...
但还不够,这只知道了每个库隐射到哪段虚拟内存,我还需要知道实际的物理内存。此时需要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.so
Size: 24 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 24 kB
Pss: 6 kB
Shared_Clean: 24 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 24 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
VmFlags: 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> : ", $6
if ($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
查看物理内存就到底了吧。平时注意理论积累,在关键的时候才不会像无头苍蝇。