【4】内存工具-文件页内存拆解指导
来源:飞书 wiki(obj_token: CPBHdau0wormf3xExPwcI6NInRg)
注:本文档内 9 张图片及 1 个 gzip 附件的 media_download 均返回 403(文档内嵌资源 token 无下载权限),已逐条记录为占位引用。
有问题可咨询 @印闯 ,FAQ一定要看,不回答文档上已有内容
什么是文件页内存
进程在启动和运行过程中会加载 so 库及各类资源文件(如数据文件、日志模板、字典/模型等)。这些文件通常通过mmap映射到进程的虚拟地址空间,但映射建立后并不会立刻把文件内容全部读入内存。当业务逻辑首次执行到相关代码路径、触发函数调用或访问映射区域内的数据时,对应虚拟页会发生按需调页(page fault)这些被映射并驻留的文件页会计入进程的内存占用,表现为进程 RSS 随访问逐步增长。
解决什么问题
- 问题背景
业务进行内存拆解时会遇到某些库的内存占用比较大,以重启surfaceflinger后libsurfaceflinger.so为例子,整个库的size为10376KB,RSS占用为6628KB,业务希望能看到这些内存都是谁访问的,进一步做后续的优化
➜ filemap_fault git:(filemap_fault_local) ✗ adb shell showmap 3318 | grep "libsurfaceflinger.so|rss" -iE
size RSS PSS clean dirty clean dirty swap swapPSS HugePages PmdMapped PmdMapped Hugetlb Hugetlb Locked # object
10376 6628 6628 0 0 6292 336 0 0 0 0 0 0 0 0 4 /system_ext/lib64/libsurfaceflinger.so-
工具达成的效果:区别于文件页内存调试分析路线(不需要drop cahche、共享库的访问也可以抓到、可统计到库的所有页面被访问的调用栈)理想场景(如重启surfaceflinger场景)进程中各个库被工具抓到的访问的size和库的rss几乎可以匹配
📷 图片描述: 图片展示的是内存工具文件页内存拆解指导中,解决业务进行内存拆解时遇到某些库内存占用大问题时的工具效果。图中以重启surfaceflinger后libsurfaceflinger.so为例,呈现了Lib Tl、RSS Tl、PageTl(Size / RSS)Tl、VMAP Tl等信息,其中红色框突出 addCriterion。 | token
Ahv1bUQBCoZsw7xAAWochbiunmb -
支持直接查看进程中某个库访问的火焰图:有size和count俩个维度的统计
📷 图片描述: 图片是一张表格,展示了/system_ext/lib64/libsurfaceflinger.so库的相关内存数据。表格包含RSS、ToolSize、Pct(ToolSize / RSS)、VMAs等列。其中,RSS为6.47MB,ToolSize也为6.47MB,Pct(ToolSize / RSS)为100.00% ,VMAs显示为4行。 | token
IXTAb3mhKoI45MxQ1LgcvWh9n8l
📷 图片描述: 图片展示的是内存工具文件页内存拆解指导中,重启surfaceflinger后libsurfaceflinger.so的火焰图。图中以不同颜色条块呈现不同函数的调用情况,红色条块较多,代表调用次数较多。底部有函数名称及所属模块标识,如”do_page_fault""do_pte_miss”等。 | token
R2GDbNUEAoHiCbx8EEUcJAVpnoh
如何使用
数据采集: 支持windows、wsl、linux平台
开始采集文件页访问调用栈
开机&&典型场景内存:参考【3】内存工具-命令行方式使用
老化场景内存:参考进程老化模型使用说明文档中内存类型选择filemap
📷 图片描述: 该图片展示了一张表格界面,顶部导航栏选中了”特定参数”选项卡。表格内包含模型特定参数的配置信息,参数名称均以”mem_use_“开头,涵盖设备名称、测试场景、进程名称、启动时机、选项设置等内容,对应默认值如p1、systemui、com.android.systemui、before_test等。其中重点突出了两条参数说明:一条是”mem_use_options_e”,默认设为native,支持native、ringbuffer、filemap三种模式;另一条是”mem_use_options_b”,设置为256,对应设置mem_use的事件buffer大小。 | token
WUj4bxc7loXbSfxGmKZc3wB9nIg
查看输出文件是否完整
➜ test-filefault git:(v1) ✗ tree -L 1
.
├── 1.smaps
├── 2.smaps
├── 3.smaps
├── memuse.filename.data
├── memuse.info.data
├── memuse.kernel.stack.data
├── memuse.stack1.data
├── memuse.stack2.data
└── symbol_data
└── smaps_all // 只有老化场景需要,开机&&典型场景不是必须的数据解析:
老化内存数据
网站解析(推荐)
最新的老化测试框架采集数据后会:自动上传、解析、通知用户
本地解析
python3 ${CODE_ROOT}/scripts/MemHookDataHnadleScripts/DataPostProcess/filemap_fault/report_filemap_fault_events_peak_and_full.py \
-code-root /path/to/memscalpel \
--data-dir /path/to/test-filefault \
--out-dir /path/to/out开机&&典型内存场景
网站解析(暂未支持)
本地解析支持wsl、linux平台
📎 附件:开机&&典型内存场景本地解析脚本包(gzip,application/x-gzip)。 | token
Y91wbWUowomFYkxKeI5c58UWnsf(下载 403 失败)
python3 ${CODE_ROOT}/scripts/MemHookDataHnadleScripts/DataPostProcess/filemap_fault/report_filemap_fault_events.py \
-code-root /path/to/memscalpel \
--data-dir /path/to/test-filefault \
--out-dir /path/to/outpython3 ${CODE_ROOT}/scripts/MemHookDataHnadleScripts/DataPostProcess/filemap_fault//report_filemap_fault_events.py \
-code-root /path/to/memscalpel \
--data-dir /path/to/test-filefault \
--smaps /path/to/systemui_dijun_500250000000_iter1_smaps_42.txt \
--start-ts 0 --end-ts 500250000000 \
--out-dir /path/to/out输出解读:
主html:faults-toggle-embedded.html
- “文件页访问火焰图”:目标进程所有库文件被访问路径按照调用栈合并后的火焰图,支持按照size/count俩个维度显示(左上角切换),火焰图支持搜索某个函数高亮、点击某一层缩放到具体调用栈
📷 图片描述: 图片展示的是文件页访问火焰图,目标进程所有库文件被访问路径按照调用栈合并后的结果。图中以不同颜色的方块呈现,颜色深浅代表调用栈中函数的出现频率,颜色越深频率越高。底部有函数名称标识,如”filemap_fault""do_page_fault”等。左上角有”size""count”切换按钮,可按不同维度显示数据。 | token
DtpwbILD6oS0jUxrY7LcgKarn7O
2) “库访问覆盖度”:目标进程所有库文件的采集结束时RSS的值和工具统计到该库被访问的size值对比,支持筛选库、各列支持排序(默认RSS降序)
📷 图片描述: 图片展示的是文件页访问火焰图的输出解读中”库访问覆盖度”部分。表格中列出了多个库文件信息,包括Lib文件路径、RSS %、ToolSize、PctToolSize / RSS %、VMA s %等。其中,最后一列”Links”中以红色框突出显示,支持点击。 | token
GF0ebssg6oZ1qLxaETbcweYhnUf
最后一列Links中支持点击:html是拆解出来属于这个库的被访问的调用栈、csv是这个库的每次被访问的信息(进程、线程、时间戳、fault的地址、对应库的偏移等)
📷 图片描述: 图片展示的是内存工具文件页访问火焰图。左侧为调用栈列表,显示了如”zte_cdc_info @ /kernel”等库文件的调用栈信息。右侧是火焰图,以不同颜色块呈现调用栈的访问频率,颜色越亮表示访问次数越多。图中还标注了”Search”搜索框。 | token
V62Obo7sRotnXhxbSgUcDcfInse
📷 图片描述: 图片展示的是内存工具文件页内存拆解指导中”库访问覆盖度”输出解读内容。表格中包含pid、tid、mem_type、timestamp_ns、fault_add、page_addr、vm_start、vm_end、lib、lib_off等列。其中,lib列以绿色框突出显示,列出了多个库文件名称,如zte_cdc_info等。 | token
ZsHZbE9Eco6tCyxIgW1cMYqvnOb
FAQ
关于”库访问覆盖度”统计到的值说明
我们重点关注采集期间库文件被访问的情况,然后做针对性的优化
🌟 注1:我们统计的是某个时间段的对于文件页的访问,如果统计开始前so本身就占据一定的RSS,这块不在我们统计范围内,自然最后的size和RSS会存在差异 注2:我们采集的是文件页被访问的调用栈,不包含文件页释放: 1)释放后又被重新访问的,我们会丢弃较早访问的调用栈(同一页(4KB page)只保留最后一次出现的调用栈) 2)释放掉没有被重新访问的,这里会保留被释放部分内存对应的访问调用栈,此现象在程序长期运行期间肯呢个出现频率会大
调用栈中某一次访问的size大于一页(4K)
🌟 理想状态下每次fault只会触发一个页面(一般是4K)的加载,但由于faultaround、大页等内核特性影响,会出现单次fault导致一次性加载多个页的情况,工具针对faultaround、大页做了适配处理,所以会出现采集到的size大于一页的情况,业务也可以选择关闭俩个特性来测试,或者保持现状 关闭fault_around:echo 4096 > /sys/kernel/debug/fault_around_bytes 关闭大页:setprop persist.sys.mthp.enabled 0
火焰图打开的时候会有小部分的mock_func_miss现象,
工具定位中~~~,当前占比不大10%左右,业务可优先分析已有的调用栈
📷 图片描述: 图片展示的是火焰图中关于so库的文件页访问和释放的调用栈信息。图中以不同颜色标识了多个函数调用,如art::Thread::…、__pthread_start(v…)、__start_thread @…等,其中mock_func_miss @ /xxx/xx/libmock.so被红色框线突出显示。 | token
J2aubymrrou1oHxqaXccg0Ghnod
关于so库的文件页访问和释放
1. 加载阶段:linker 解析 ELF 元数据
- dlopen/程序启动加载 so 时,linker 会读取 ELF header、program header、dynamic section、符号表/哈希表、重定位表等元数据,触发相关文件页的按需调页(page fault)。
2. 加载阶段:初始化代码执行
- so 的 init_array、全局构造函数、初始化函数会在加载阶段被执行;其中对代码段(.text)和只读数据段(.rodata)的访问会触发相应页的调入。
3. 使用阶段:业务代码路径触发 .text/.rodata 的按需调页
- 业务调用库函数会首次执行对应 .text 页;函数内部访问常量表、字符串、跳转表等会触发 .rodata 页。由于页粒度与局部性,同一热点路径常表现为一串相邻页被逐步触发。
4. 异常/回溯/采样导致的 unwind 元数据访问
- crash 打栈、backtrace()、profiler 栈回溯、C++ 异常展开会读取 .eh_frame/.eh_frame_hdr/.gcc_except_table 等 unwind 元数据,从而触发这些元数据所在文件页的访问。 1. 内核 page cache 回收(内存压力驱动)
- so 的 .text/.rodata 等通常是 file-backed clean pages,在内存紧张或后台回收时会被内核从驻留集合中逐出(可随时从文件重新读取)。
2. 显式丢弃缓存/回收建议(应用或运维触发)
- 运维侧:echo 3 > /proc/sys/vm/drop_caches 等会清理 page cache(并非只针对某进程)。
- 应用侧:posix_fadvise(DONTNEED)、对映射区域 madvise(DONTNEED) 等可能促使相关文件页更快变为可回收。
3. 解除映射:dlclose / munmap(进程地址空间层面的"释放")
- 若业务动态加载/卸载库,dlclose 可能导致对应映射被解除(munmap),进程不再持有该 so 的 VMA。其他
adb shell
# 打开内核符号表
echo 0 > /proc/sys/kernel/kptr_restrict
# 关闭fault_around,
echo 4096 > /sys/kernel/debug/fault_around_bytes
# 关闭read_ahead,理论上不影响
echo 4 > /sys/block/sda/queue/read_ahead_kb
# 关闭大页
echo never > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled
关闭大页:setprop persist.sys.mthp.enabled 0
开启大页:setprop persist.sys.mthp.enabled 1
# 关闭启动场景文件预读,理论上不影响
setprop persist.sys.stability.PrereadEnable false
# 丢cache
echo 3 > /proc/sys/vm/drop_caches
同一页(4KB page)只保留最后一次出现的调用栈一些优化建议
1. 减少 so/资源文件体积(从源头减少需要触达的页)
1. 裁剪不必要功能、拆分不常用模块为可选库/插件。
2. 去掉大而低频的表驱动资源(大字典、规则表、模型)或做分片加载。
2. 控制加载阶段的初始化(减少 init 导致的页触达)
1. 避免在 init_array/全局构造里做重计算、扫大表、读配置/模型、建大缓存。
2. 将初始化拆成"必需轻量 + 按需/异步重活",把重逻辑延后到真正需要时。
3. 使用阶段按需对映射区域 madvise(DONTNEED),有性能劣化风险