【4】内存工具-文件页内存拆解指导

来源飞书 wiki(obj_token: CPBHdau0wormf3xExPwcI6NInRg)

注:本文档内 9 张图片及 1 个 gzip 附件的 media_download 均返回 403(文档内嵌资源 token 无下载权限),已逐条记录为占位引用。

有问题可咨询 @印闯 ,FAQ一定要看,不回答文档上已有内容

什么是文件页内存

进程在启动和运行过程中会加载 so 库及各类资源文件(如数据文件、日志模板、字典/模型等)。这些文件通常通过mmap映射到进程的虚拟地址空间,但映射建立后并不会立刻把文件内容全部读入内存。当业务逻辑首次执行到相关代码路径、触发函数调用或访问映射区域内的数据时,对应虚拟页会发生按需调页(page fault)这些被映射并驻留的文件页会计入进程的内存占用,表现为进程 RSS 随访问逐步增长。

解决什么问题

  1. 问题背景

业务进行内存拆解时会遇到某些库的内存占用比较大,以重启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
  1. 工具达成的效果:区别于文件页内存调试分析路线(不需要drop cahche、共享库的访问也可以抓到、可统计到库的所有页面被访问的调用栈)理想场景(如重启surfaceflinger场景)进程中各个库被工具抓到的访问的size和库的rss几乎可以匹配

    📷 图片描述: 图片展示的是内存工具文件页内存拆解指导中,解决业务进行内存拆解时遇到某些库内存占用大问题时的工具效果。图中以重启surfaceflinger后libsurfaceflinger.so为例,呈现了Lib Tl、RSS Tl、PageTl(Size / RSS)Tl、VMAP Tl等信息,其中红色框突出 addCriterion。 | token Ahv1bUQBCoZsw7xAAWochbiunmb

  2. 支持直接查看进程中某个库访问的火焰图:有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/out
python3 ${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

  1. 文件页访问火焰图”:目标进程所有库文件被访问路径按照调用栈合并后的火焰图,支持按照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),有性能劣化风险