自定义应用 phasedmem 模拟相机的分阶段内存申请行为:
| 阶段 | 动作 | 模拟对象 |
|---|---|---|
| STAGE1 | 启动即申请 1GB = 512MB 匿名 zero-fill + 512MB mlock wired | IOSurface pageable 形态 + 驱动 wired 形态 |
| STAGE2 | 4 步 × 256MB(128 匿名 + 128 wired),步间隔 5s → 2GB | 相机管线扩容 |
| SETTLE | 静置 30 秒 | 稳态观察 |
| RELEASE | munlock 全部 wired → munmap 全部 | 用户态释放效率 |
采样:0.5s 粒度整机 vm_stat 全字段 + appscan 进程级 footprint 轨迹;工具自带毫秒级自计时。
| 字段 | 起点 | 终点 | Δ | 解读 |
|---|---|---|---|---|
| Pages free | 181,040 | 181,626 | +9MB | 净零——申请的都还回去了 |
| wired down | 44,780 | 44,835 | +0 | 1GB wired 全额摘除 |
| compressor stored | 81,809 | 86,190 | +68MB | 系统背景噪声(其它 App churn) |
前置:拉起 抖音 / 淘宝 / 京东 / 网易云音乐 / bilibili 五 App 至满载(free 跌至 2,310 页 = 36MB)。
| 供给来源 | 数量 | 机制 |
|---|---|---|
| 五 App 匿名页压缩 | −1,277MB | vm_pageout 同步压缩供货(场景期间 comp stored +1,048MB 内容,物理占用仅 +340MB → 压缩比 3.1:1) |
| file-backed 摘除 | −199MB | 代码页/缓存页直接丢弃(可从磁盘回读) |
| free 池消耗 | −21MB | 几乎没动——本来就只有 36MB |
| 击杀进程 | 0 个 | 五 App 全部存活,纯页级腾挪 |
| App | footprint | 内驻 | 压缩态 | 压缩占比 |
|---|---|---|---|---|
| 抖音 Aweme | 533.8MB | 1.3MB | 503.5MB | 94% |
| 淘宝 | 315.4MB | 0.8MB | 286.3MB | 91% |
| 京东 | 130.8MB | 2.3MB | 128.1MB | 98% |
| 网易云音乐 | 289.3MB | 157.8MB | 137.6MB | 48% |
| bilibili(番剧版) | 233.4MB | 151.5MB | 81.1MB | 35% |
注:网易云/bili 的内驻偏高,因实验结束后部分页已被访问解压回驻。
| 操作 | 场景一(free 2.83GB) | 场景二(free 36MB) | 退化幅度 |
|---|---|---|---|
| STAGE1 1GB(512a+512w) | 259ms(3.95GB/s) | 392ms(2.61GB/s) | ×1.52 |
| — touch 匿名 512MB | 137ms(3738MB/s) | 108ms(4732MB/s) | 更快* |
| — touch+wired 512MB | 231ms(wire 6491MB/s) | 167ms(wire 7962MB/s) | 更快* |
| grow 每步 256MB | 61~76ms(3.3~4.1GB/s) | 58~95ms(复测) | ≈持平 |
| STAGE2 全程(4×256MB) | 20.3s(含 5s 间隔) | 20.4s(含 5s 间隔) | 持平 |
| 释放 2048MB | 144ms | 143ms | 完全一致 |
| — munlock 1024MB | 95ms(10.3GB/s) | 95ms(10.3GB/s) | — |
| — munmap 2048MB | 49ms(41.8GB/s) | 48ms(41.8GB/s) | — |
* 场景二 touch 更快的原因:五 App 的页预先已在压缩器中"热身",zero-fill 直接消耗刚释放的 free 页,无需唤醒 pageout 扫描器。STAGE1 总时长变慢在于首秒同步压缩五 App 供货的等待。
应用侧:启动即 1GB(512MB 匿名 zero-fill surface 模拟 + 512MB wired 驱动模拟),增长到 2GB(各 1GB)。进程 footprint 精确匹配(1025.6MB → 2050.1MB,与申请量差 <0.1%)。
系统侧供给量:场景一全从 free 池划扣(free 2.83GB 充足,申请后仍剩 825MB);场景二 free 仅 36MB,系统从"压缩五 App 匿名页(−1,277MB)+ 摘 file-backed(−199MB)"腾出 1.5GB+ 供货,压缩器物理占用净增仅 +340MB(内容 1,048MB,压缩比 3.1:1)。内存类型上:匿名页走 zero-fill 分配(计 zero_fill 计数器),wired 走 mlock→vm_page_wire(不可回收、全记账、释放前纹丝不动)。
1GB 启动申请 259ms(空闲)/ 392ms(满载),综合 2.6~4.0GB/s;分解后 touch 匿名 3.7~4.7GB/s、wire 6.5~8.0GB/s。free 不足时的供给机制:同步压缩——mlock 触发的缺页路径上,vm_pageout 直接压缩五 App 的冷匿名页(无需先写盘、无需杀进程),每步 128MB wired 供给等待 <100ms。整个 2GB 满载过程 零击杀、五 App 全存活。
| 统计值 | 场景一变化 | 场景二变化 | 合理性判定 |
|---|---|---|---|
| Pages free | 181k→181k(净零) | 3k→91k(+1.4GB) | ✓ 一:释放即回吐;二:释放后五 App 未完全回驻,页暂存 free |
| wired down | +1,024MB 后归零 | +1,024MB 后归零 | ✓ mlock 全额记账、munlock 全额摘除,两场景分毫不差 |
| Anonymous | −116MB(净) | −1,277MB | ✓ 二:五 App 页被压后从 anon 转入 compressor 记账 |
| compressor stored | +68MB(噪声) | +1,048MB 内容/+340MB 物理 | ✓ 压缩比 3.1:1(WKdm 典型值) |
| File-backed | 持平 | −199MB | ✓ 系统摘冷代码页供货(可磁盘回读,最便宜来源) |
| 进程 footprint | 峰值 2050.1MB 全解压 | 峰值 2050.1MB(33% 压缩态) | ✓ 同为 2GB 但驻留形态不同——footprint 含压缩态 |
当 free 不足时,系统回收文件页的顺序是什么?本节用源码(xnu-7195.141.2)+ 两个专门实验回答:文件页由哪些成分组成、每种成分的回收优先级、以及地板保护在哪里生效。
| 成分 | 性质 | 回收成本 | 本机实测占比 |
|---|---|---|---|
| 投机页(speculative) | 预读簇读入但从未被访问(clustered read-ahead 超出请求范围的部分) | 零——直接丢弃,无 I/O | 基线 25,474 页(398MB)= fb 的 21%(压力后重建池约 3,000 页) |
| 冷文件页(inactive external) | 代码段(dylib 文本,来自 dyld 共享缓存)、mmap 的资源文件、字体文件、资产目录——曾被访问后老化 | 低——clean 直接丢弃;被再次访问时从磁盘 pagein 回来(约 200μs/页) | fb − spec ≈ 95,259 页(1.49GB)= 79% |
| 脏文件页(dirty) | mmap 可写映射被写过的页 | 高——必须先写回(pageout)变 clean 进 cleaned 队列才能回收 | 本实验工作负载下接近 0(两场景 Pageouts 仅 +73 页 / +1MB) |
| 活跃文件页(active) | 近期访问过的代码/资源(热路径) | 最高——需先老化降级到 inactive 才可回收 | 计入 active,不单列 |
free_count = vm_page_free_count + vm_page_speculative_count);"File-backed" = speculative + 活跃文件页 + 冷文件页 + cleaned。所以 speculative 是文件页的子集,也是"可用内存"的隐藏分量。pageout 扫描器 vm_pageout_scan 每轮从多个队列按优先级挑选牺牲页(xnu-7195.141.2 osfmk/vm/vm_pageout.c):
| 优先级 | 队列 | 源码锚点 | 语义 |
|---|---|---|---|
| ① 最优先 | cleaned Q | vm_pageout.c:2359-2365 | 脏页已经写回变 clean 的页——I/O 成本已付,直接摘除("Try for a clean-queue inactive page... Pick them up now that they are clean") |
| ② 次优先 | speculative aged Q | vm_pageout.c:2367-2378 | 预读但从未被触摸、且已度过 10×500ms=5 秒老化保护期的投机页——零 I/O 直接丢 |
| ③ | background Q | vm_pageout.c:2380-2419 | 后台模式(darkwake)下的 external 页(CONFIG_BACKGROUND_QUEUE) |
| ④ | inactive external Q | vm_pageout.c:2448+ | 普通冷文件页——但受 filecache_min 地板保护:低于地板时改 reactivate 放回(:2475-2490,计数器 vm_pageout_filecache_min_reactivated) |
| ⑤ 兜底 | inactive internal Q | vm_pageout.c:2505+ | 匿名页——送压缩器(不是丢弃) |
三个调节器(同一文件):
1.5GB 匿名压力在 0.5 秒内施加(工具 anonpress.c,ios-probe/cli),单采样窗口内的供给来源分解:
投机池在单个 0.5s 窗口内从 25,474 页被排空到 239 页(−398MB)——这正是源码里 speculative aged Q 排在 inactive external 之前的实测映照:零成本牺牲品先死。同时 fb 从 120,733 → 79,161(−650MB),其中 398MB 来自投机池、255MB 来自冷文件页。
逐级加压 6 × 512MB(每级稳 7 秒)观察 fb 的阶梯响应——用于区分"fb 不降"是地板保护还是"不再需要回收":
| 累计压力 | free | spec | file-backed | fb 较上级 | 压缩次数(累计Δ) | reactivated(Δ) | 供给主力 |
|---|---|---|---|---|---|---|---|
| 512MB | 92,633 | 3,156 | 82,627 | +2MB | 0 | 10 | free 池 |
| 1,024MB | 59,616 | 3,221 | 82,715 | +1MB | 0 | 54 | free 池 |
| 1,536MB | 26,606 | 3,261 | 82,795 | +1MB | 0 | 60 | free 池 |
| 2,048MB | 3,080 | 620 | 76,659 | −95MB | 3,464 | 1,090 | free 枯竭 → 投机池排空(3,261→620) |
| 2,560MB | 2,810 | 628 | 54,948 | −339MB | 17,038 | 11,652 | 冷文件页大额摘除 + 压缩器启动 |
| 3,072MB | 3,010 | 598 | 54,821 | −1MB | 50,010 | 11,741 | fb 钉死地板,全部靠压缩器 |
三个决定性信号同时出现:
vm_pageout_filecache_min_reactivated 路径(vm_pageout.c:2480-2490)的签名——扫描器选中了地板以下的文件页,但拒绝偷走、放回活跃队列,转头去压缩匿名页。| 实验 | file-backed 变化 | 解释(用本节顺序链) |
|---|---|---|
| 场景一(free 2.83GB 充足) | 持平(+71MB 噪声) | free 池全额支付,回收链根本未被唤醒——供给顺序第 0 级 |
| 场景二(五 App 满载 free 36MB) | −199MB | 首波冲击的冷文件页摘除(顺序链 ④ 级)+ 投机池排空;之后 fb 钉在 97k 页不再降——同为 filecache_min 地板 |
| 本节瞬时冲击(1.5GB/0.5s) | −650MB | 投机池全额(−398MB)+ 冷文件页(−255MB)单窗内依次支付 |
| 本节阶梯加压(3GB) | −432MB | 自由段(1.5GB 内)不动 → 投机池 → 冷文件页 → 856MB 硬地板后全靠压缩器 |
uiopen 静默失败(设备老毛病),重启 SpringBoard 无效后整机重启——Taurine 半不受限越狱丢失,人工重新越狱后恢复(工具在 /var/mobile 重签 ent.plist 后继续)。tv.danmaku.bilianime,进程名 bili-universal。