管线需求 ≈1.0GB,与内存环境无关(两场景 Δwired +987MB vs +996MB,差<1%)。归属四份:①内核无主 wired(ISP DMA 环形缓冲/驱动固件)≈375MB;②mediaserverd iokit_mapped(IOSurface 管线缓冲)+612MB;③Camera 壳进程仅 26.1MB;④backboardd 显示侧 +22.9MB。整机抽取:场景一从 free 池抽 -1,639MB(管线+框架缓存副作用);场景二 free 只动 +42MB,全部由回收供给(文件页逐出 -891MB+压缩净得 348MB+purgeable/spec 即摘 ~60MB)。
进程出现→管线内存全部就位 2.04s(场景一)/ 1.77s(场景二),加 SB 派生亚秒级,冷启动到预览稳定 ≈2.1–2.9s。分配机械极快:wire 峰值 3,592MB/s(单 205ms tick +738MB)——慢在管线编排不在分配。free=48MB 时照样 1.77s 就位:回收机器水位驱动,供给顺序 spec/purgeable 即摘(零成本)→ 文件 clean 页逐出 -891MB(≈380MB/s、零 I/O)→ 匿名压缩 +474MB gross(峰值 682MB/s)。jetsam/freeze 全程零触发,五 App 全存活。
所有计数器变化均可由机制解释:场景一零压缩零逐出(compressions/purges/swapouts 全程恒定),free −-1,639MB=管线+框架文件缓存(active +521MB、文件页 +262MB);场景二呈镜像结构——free 探底 44MB 不枯竭、压缩器陡涨(stored +1,474MB 全程)、speculative/purgeable 被即摘、reactivations +780MB(框架从缓存直接拉回)。杀相机后 wired bulk 回落(3,505MB/s)+驱动 teardown 尾 ~8s;存活 App 的压缩页不回吐(单向阀)。无泄漏迹象。
同一设备同一协议独立复测:管线需求不变量再次成立(Δwired 两轮均在 0.99–1.02GB);本轮新增 ①进程级 8 目标全程 footprint 曲线(250ms)②相机窗口逐出/压缩的分离账本 ③freeze 计数器取证(5 App 阶段冻结 2 进程 +154MB,相机窗口零新增)。结论与前轮一致,量化口径更细。
都不是主角——架构才是。补充实验实测:零页 MZV 波爆发 1310MB/s、真实数据特征波 975–994MB/s×1s,且全部工作落在 E 簇(能效核,系统态 0.8→1.1 核),P 簇全程空闲——2 个优先级 91 的专用压缩线程绑 E 簇(vm_pageout.c:4332)+ 每核 ~780MB/s 的 WKdm 手写汇编 + 需求不在回收路径上串行。Linux 复刻吞吐无障碍(lz4≥WKdm),差距在执行位置与默认策略(详见 §7)。
不是免回收,是回收在别的线程上并行跑。锁页线程是纯消费者:拿不到页就 VM_PAGE_WAIT 睡觉(vm_page_wait 无一行回收代码);三批生产者线程(scan+压缩机×2,优先级 91)并行补货。实证:相机第一个 250ms 窗口 wire 爆发 1897MB/s,同窗口供给(逐出 1324+压缩 682)= 2006MB/s 同步开动,free 探底 44MB 后在 wire 爬坡最猛阶段不降反升回到 126MB(详见 §8)。
释放原语一样便宜,策略完全不同。XNU 用固定成本瀑布把回收最大比例导向最便宜路径(cleaned→投机型→文件页,匿名页连拿 2 个即限流,文件缓存 14% 地板,"压缩器积压时继续偷文件页"——vm_pageout.c:2145 注释自白);Linux 用 swappiness×refault 成本比例模型追求全局最优,且直接回收在申请者上下文执行。Linux 领先项:workingset refault 距离(逐错保护更精确)(详见 §9)。
| 场景一:后台干净 | 场景二:五大 App 后台 | |
|---|---|---|
| 前置状态 | kill -9 全部后台应用(36 进程+补杀),vm_stat 确认 Pages free = 139,208 页 = 2,175MB > 2GB;采样起点 free 2,475MB | 依序启动 抖音→淘宝→京东→网易云音乐→bilibili(各 20s 间隔,uiopen 拉起),五 App 全部在案后采样基线 |
| 相机动作 | uiopen --bundleid com.apple.camera,ps 尾锚验证进程存活,预览静置 ≥30s(7.2s–43.4s 窗口),随后 kill -9 观察拆除 | 同左(112.3s 启动,150.5s kill;预览静置 33s) |
| 基线水位 | free 2,475MB(充裕)· level 67% | free 97MB(贴地)· level 40% → 相机期 23% |
为消除旧方案"每秒 spawn vm_stat 自污染 ~180 页/样本"的问题,本轮用纯内核调用的常驻采样器 camwatch(arm64 CLI,ldid 签名 task_for_pid-allow 等权限):
host_statistics64(HOST_VM_INFO64) 全量 20 个计数器(wired/free/active/inactive/speculative/purgeable/purges/文件页/匿名页/压缩器/compressions/decompressions/swapins/swapouts/pageins/pageouts/faults/cow/zero-fill/reactivations)+sysctl kern.memorystatus_level(jetsam 水位%);sysctl KERN_PROC_ALL 匹配 8 个目标进程(Camera / mediaserverd / backboardd / 五 App)→ task_for_pid+TASK_VM_INFO 取 phys_footprint / internal / compressed——进程账与整机账同 tick 对齐;date 仅 1s 分辨率的坑);测量窗口内零额外进程 spawn(编排决策全部来自 camwatch 输出流的实时解析);图 1-1 场景一全程(85s,200ms/点)。wired 从 795MB 抬到 1,782MB(Δ+987MB);free 从 2,475MB 降到 836MB(Δ-1,639MB);mediaserverd footprint 34→731MB。杀相机后 free 全额回位。
图 1-2 启动微观(4.5–9s)。三段式:①壳进程加载(Camera fp →26MB)②mediaserverd 建链(iokit 映射涌入)③集中 wire:单 205ms tick 内 wired +738MB(3,592MB/s)。
图 1-3 杀相机后的回收(43–52s)。kill 后 ≤200ms 进程消失,wired bulk −906MB/202ms;mediaserverd 678→46MB;随后 ~8s 驱动 teardown 慢尾把 wired 补回基线。
| 计数器(vm_stat) | 变化 | 机制解读 |
|---|---|---|
Pages wired down | +987MB | 相机管线主体:ISP DMA 缓冲+IOSurface 锁页+驱动固件结构(Q1 详拆) |
Pages free(含 speculative) | -1,639MB | 充裕环境直接从 free 池抽取;回收机器纹丝不动 |
Pages active | +521MB | 相机/mediaserverd 框架文件页+管线用户态结构进入活跃队列 |
Pages inactive | +142MB | 启动过程置换出的冷页入 inactive |
Pages speculative | +88MB | free 充足时预读大胆落 bins(对照场景二被即摘) |
Pages purgeable / purges | +18MB / +8MB | 无需丢弃任何可清除页 |
file-backed pages | +262MB | 框架/资源文件页缓存增长(启动的副作用,非管线本体) |
anonymous pages | +489MB | mediaserverd 内部堆(15.7→108MB)+Camera 壳 18MB+系统进程伴随增长 |
compressions / decompressions | 0 / +42MB | 零压缩——这是"free 充裕则压缩器不劳动"的直接证据;decompressions 为系统进程触碰旧压缩页的背景噪声 |
pageins / pageouts / swapins / swapouts | +242MB / 0 / 0 / 0 | 只读入不换出;启动读入即相机框架代码与资源 |
reactivations | +0MB | ≈0:free 充裕时无需从 inactive 拉回 |
图 2-1 场景二全程(185s,250ms/点)。五 App 依序启动把 free 从 2,192MB 压到 97MB;压缩器(右轴色)同步吃进后台 App 驻留页;相机启动(112.3s)与 kill(150.5s)的 wired 起落。
| 计数器 | 变化(pre→相机前基线) | 解读 |
|---|---|---|
Pages free | -2,096MB | 2.2GB 被 5 个重 App 吃光(抖音 567 / 淘宝 320 / 京东 146 / 网易云 350 / bilibili 358 峰值 footprint,合计 1,741MB) |
compressions(累计) | +1,474MB | 后台化 App 的驻留匿名页被渐进压缩(淘宝 in≈0.4MB 几乎全压缩;抖音 compressed 434MB) |
Pages occupied by compressor | +394MB | 压缩器占用净增;全程序压缩比 ≈2.9:1(stored 内容量/占用) |
swapouts | +157MB | ≈freeze_pageouts 增量 9,869 页(154MB)——freezer 冻结 2 个后台进程落盘(freeze_count 0→2),非传统换出 |
pageins / reactivations | +2,906MB / +1,457MB | 五 App 冷启动读入+相互顶替时的缓存回拉 |
file-backed / anonymous | +323MB / +957MB | App 代码资源缓存+用户态堆驻留 |
图 2-2 相机窗口微观(110–122s)。wired +996MB 与压缩量陡涨同步;free 探底 44MB 不枯竭(供给跟上了分配)。
| 供给来源(回收侧) | 量 | 成本/速率 |
|---|---|---|
| 文件页逐出(file-backed) | -891MB | clean 页零 I/O 直接回 free;≈380MB/s 持续 |
| 匿名压缩(compressions) | +474MB gross | 峰值 682MB/s(单 tick);净得 348MB(压缩器占用 +126MB) |
| purgeable 丢弃 | -37MB | ≈0 成本即摘(网易云 volatile 12MB 等) |
| speculative 摘除 | 瞬时 −23MB | spawn tick 内即摘;随后相机预读又回填 +61MB(净 +61MB) |
| inactive / active 队列 | -620MB / -538MB | 队列腾挪:冷文件页出队+框架热页经 reactivation(+780MB)直接复用缓存 |
| 未动用的手段 | jetsam 0 次 · freeze 0 次新增 · swap 0 | 页级腾挪足够,无需进程级献祭 |
图 2-3 五 App 在相机窗口的形态迁移:蓝(驻留匿名)→紫(已压缩)。bilibili internal 278→152MB(其中 ~33MB 被直接丢弃:reusable/purgeable 路径),网易云 216→187MB;淘宝在基线前已被压干(in≈0)。
| App | fp·基线 | internal·基线 | compressed·基线 | fp·预览后 | internal·后 | compressed·后 | Δfp |
|---|---|---|---|---|---|---|---|
| 抖音 | 566 | 89 | 434 | 566 | 48 | 476 | +0 |
| 淘宝 | 315 | 0 | 300 | 315 | 0 | 300 | +0 |
| 京东 | 138 | 104 | 34 | 138 | 89 | 49 | +0 |
| 网易云 | 298 | 216 | 93 | 298 | 187 | 121 | +0 |
| bilibili | 279 | 278 | 0 | 246 | 152 | 93 | -33 |
五 App footprint 合计 1,597→1,564MB:压缩只改变形态(驻留→压缩)不改变记账;净 fp 变化 −33MB 全部来自 bilibili 的可丢弃页。相机杀掉后五 App 全部存活(survivors.txt)。
wired bulk 回落 -1,015MB(峰值 3,473MB/s),mediaserverd 642→50MB,free 回 +1,239MB;压缩器占用 −-29MB(死会话压缩页直接摘除,不经解压),但存活 App 的压缩页不回吐(compressions 只增不减的单向阀)。拆除期 pageins +102MB——系统进程回拉缓存。
| 归属 | 场景一 | 场景二 | 内容 |
|---|---|---|---|
| 内核无主 wired | ≈375MB | ≈471MB | ISP DMA 环形缓冲、驱动固件结构、新映射页表——只进全局 wired 计数器,任何进程 footprint 都看不见 |
| mediaserverd iokit_mapped | +612MB(1.9→614) | +525MB | 相机管线真正的主人:AVFoundation 服务端的 IOSurface/DMA 缓冲(三方共享:App 绘制/GPU 渲染/ISP 写入) |
| mediaserverd internal | +92MB(15.7→108) | + | 管线编排的用户态堆 |
| Camera 壳进程 | 26.1MB(internal 17.9MB) | 24.2MB | UI 壳:预览取景器只是管线的"遥控器" |
| backboardd 显示侧 | +22.9MB | +15.2MB | 帧提交/显示合成路径 |
| 合计(管线需求) | ≈1.05GB | ≈1.03GB | Δwired +987/+996MB 为上界口径(iokit⊂wired 有重叠,见注) |
| 阶段 | 场景一 | 场景二 | 说明 |
|---|---|---|---|
| uiopen 下发 → 进程出现 | ≤0.3–0.9s | ≤0.3–0.9s | 受 SSH 链路+时钟对齐(±0.5s)限制只能给界;SpringBoard 派生本身亚秒级 |
| 进程出现 → 壳加载完成 | ~0.8s | ~0.6s | Camera fp 爬到 ~25MB(dyld+框架) |
| 进程出现 → 管线内存全部就位 | 2.04s(wired 达平台 99%) | 1.77s | 含 mediaserverd 建链+DMA wire+IOSurface 映射 |
| 总计(下发→预览稳定) | ≈2.1–2.9s | ≈2.1–2.7s | "预览稳定"以内存平台期判定(方法学声明见 §0) |
| 操作 | 实测速率 | 含义 |
|---|---|---|
| wire 分配(峰值) | 3,592MB/s(S1)/ 1,897MB/s(S2) | 分配原语本身极快——"wired 分配慢"是误解;S2 峰值略低是回收供页的节奏,不是分配慢 |
| 管线整体就位 | 1GB / 2.04s ≈ 500MB/s 平均 | 瓶颈在管线编排(DART 映射/驱动会话协商),不在页分配 |
| mediaserverd iokit 涌入 | ~450MB/s(fp 曲线斜率) | IOSurface 批量映射 |
| 环境对比 | S1 2.04s vs S2 1.77s | free 48MB 与 free 2.4GB 的就位时间同量级——回收供给对启动速度无可感拖累(本轮 S2 甚至略快,属轮次方差) |
不是预案,是水位驱动的流水线。相机启动把 free 砸到警戒线附近(探底 44MB),回收机器按成本阶梯依次供页:
| 顺位 | 手段 | 本轮实测 | 成本 |
|---|---|---|---|
| 1 | speculative 摘除 | spawn tick 内 −23MB | ≈0(预读赌输的页,零 I/O 丢弃) |
| 2 | purgeable 丢弃 | −-37MB(purges +12MB) | ≈0(volatile 页直接扔) |
| 3 | 文件 clean 页逐出 | -891MB / 2.3s | 零 I/O(脏页才需回写,本轮 pageouts +860 页仅背景噪声) |
| 4 | 匿名压缩 | gross +474MB(峰值 682MB/s) | CPU 换内存(净得 348MB) |
| 5 | freeze(冻结落盘) | 相机窗口 0 新增(五 App 阶段已冻 2 进程 154MB) | I/O,按需后台执行 |
| 6 | jetsam(杀进程) | 0 次 | 进程献祭——页级腾挪够用时根本轮不到 |
| 统计值 | 场景一(free 充裕) | 场景二(free 贴地) | 合理性判定 |
|---|---|---|---|
Pages free | -1,639MB(含 spec +88) | +42MB(探底 44MB 后回升) | ✓ 两场景语义一致:充裕时是"水源",紧张时是"水位计"——只反映池子余量,不反映供给能力 |
Pages speculative | +88MB(大胆预读) | 瞬时 −23MB 后回填 +61MB | ✓ 与 free ⊇ speculative 的口径一致(host.c:815);"带着收益的 free",压力时第一个被摘 |
Pages wired down | +987MB | +996MB | ✓ 相机场景的主科目;两场景差<1% 证明管线需求不变量;wired 是"时变池"而非静态基线 |
Pages purgeable/purges | +18MB / +8MB | −-37MB / +12MB | ✓ 充裕时增长(缓存可丢则丢的自愿登记),紧张时即摘 |
file-backed | +262MB(框架入缓存) | -891MB(最大供给源) | ✓ 同一科目两种角色:无压力时是缓存堆积,有压力时是第一提款机 |
anonymous | +489MB | -206MB(相机增量被后台压缩抵消) | ✓ 净额=新增 − 被压缩移出,需与 compressions 联读 |
compressions/decompressions | 0 / +42MB | +474MB / +36MB | ✓ 累计型单调计数器;场景一零压缩是"够用就好"的直接证据 |
Pages in compressor | -9MB | +126MB(杀相机后 −-29MB) | ✓ 水位类;死会话压缩页直接摘除,存活 App 压缩页不回吐(单向阀) |
reactivations | +0MB | +780MB | ✓ 场景二大量回拉=框架代码页从 inactive 缓存直接复用(省 pagein);对应 pageins 仅 +194MB |
pageins/pageouts/swap | +242MB / 0 / 0 | +194MB / +860 页 / 0 | ✓ 逐出 clean 文件页零 I/O;swapouts 增量全部来自 freeze 落盘(非传统换页) |
kern.memorystatus_level | 67%(全程稳定) | 40%→23%(相机期)→46%(杀后) | ✓ 水位百分比与 free/可用页走向吻合;23% 仍未进 warn 带(kevent 零事件可佐证) |
| 操作 | 实测效率 | 场景 |
|---|---|---|
| wire 分配(峰值 burst) | 3,592MB/s(+738MB/205ms) | S1 冷启动 |
| wire 分配(压力下峰值) | 1,897MB/s | S2(回收供页节奏) |
| 管线整体装配 | ~1GB/2.04s ≈ 500MB/s | 两场景同量级 |
| spec/purgeable 即摘 | ≈0 成本(单 tick 内) | S2 spawn 瞬间 |
| 文件 clean 页逐出 | ≥380MB/s 持续(-891MB/2.3s) | S2 相机窗口 |
| 匿名压缩(burst) | 682MB/s(净收益按 2.9:1 打折) | S2 相机窗口 |
| freeze 落盘 | 154MB(后台慢速执行) | 五 App 阶段 |
| 拆除:wired bulk 回收 | 3,505MB/s(−906MB/202ms) | kill 后第一 tick |
| 拆除:mediaserverd 卸载 | 678→46MB / ~600ms | kill 后 |
| 拆除:驱动 teardown 尾 | ~8s 补完最后 ~100MB | kill 后 |
| 拆除:freeze/jetsam/swap | 0 参与 | 全程 |
| 文件 | 内容 |
|---|---|
data/s1_timeline.txt / s2_timeline.txt | camwatch 全程时间线(423 / 735 个采样点,含元数据头) |
data/s1_markers.json / s2_markers.json | 编排标记(uiopen 下发/返回、pid 首见、kill、时钟漂移) |
data/s1_pre|post_vmstat.txt, s2_pre|baseline|post_vmstat.txt | vm_stat+freeze 计数器快照(阶段锚点) |
data/s2_baseline_appscan.txt | 五 App region 级画像(相机启动前) |
data/s1_killed_procs.txt, s2_survivors.txt, s2_crashlog_recent.txt | 前置清理、存活与 jetsam 取证 |
data/comp_cpu_camwatch.txt / comp_cpu_cpuper.txt | 追问篇实验①:anonpress 零页波(MZV 路径)+每核 tick 差分(§7) |
data/dirty_cpu_camwatch.txt / dirty_cpu_cpuper.txt | 追问篇实验②:dirtyapp 真实数据特征波+每核 tick 差分(§7 核心数据) |
data/cam2_cpu_camwatch.txt / cam2_cpu_cpuper.txt | 追问篇实验③:free 充裕相机重演对照(全程零压缩,§7 对照组) |
ios-probe/cli/camwatch.c | 采样器源码(55 列 TSV 协议见文件头) |
ios-probe/cli/cpuper.c / dirtyapp.c | 追问篇新探针:每核 sys/usr/idle 差分 · 真实 App 数据特征压力源 |
run_s1.py / run_s2.py / run_compcpu.py / run_camcpu.py / run_dirtycpu.py / analyze.py / build_charts.py / build_charts2.py | 编排与分析全链路脚本 |
场景二实测相机窗口压缩爆发峰值 682MB/s(§2b)。这一节回答:这个速度是回收算法写得好还是CPU 硬件能力强?方法是三组补充实验(新探针 cpuper 每核差分+dirtyapp 真实数据压力源)+ XNU/Linux 源码对照。结论先行:都不是主角——决定性的是系统架构。压缩爆发只需要 2 个专用内核线程 × 每核 ~780MB/s 的算力(跑在能效核上),而相机的 1GB 申请根本不在这条回收路径上排队。
图 7-1 压缩波实验:单窗口压缩速率(500ms 粒度,对齐压力起点)。零页波(MZV 快速路径)峰值 1310MB/s;真实数据特征波 975–994MB/s 持续 1s——两波同数量级,说明管线(扫描摘页+unmap+队列+段存储)支持 ≥1.3GB/s,真实数据时绑定约束才落到 WKdm 算术本身。
| 实验 | 压力源与数据特征 | 压缩爆发 | 关键读数 |
|---|---|---|---|
| comp:零页波 | anonpress 2200MB 全零匿名页 | 1310MB/s(658MB/0.5s 单窗口) | 走 MZV 单值快速路径(近零算术):全程压缩比 9.2:1(+728MB 输入 → 占用仅 +79MB);free 底 30MB |
| dirty:真实数据特征波 | dirtyapp 2400MB(75% 字典型/15% 零/10% 随机,模拟实测 App 页谱) | 975–994MB/s × 1s(三窗口 234→975→994) | 全程 +1455MB 输入 → 占用 +1119MB(合成数据难压,比 1.30:1);free 底 37MB;free 贴地且无新需求时压缩器立即停工(纯需求驱动) |
| cam2:充裕对照 | free=1599MB 重演相机启动 | 0(全程零压缩) | 对照组:水位够用则压缩机器完全不劳动——与场景一同构 |
图 7-2 压缩爆发期间每簇系统态占用。E 簇(c0–c3)系统态从背景基线 ~0.8 核抬到 ~1.1 核;P 簇(c4–c5)全程 ≤0.08 核 ≈ 空闲。1s 采样窗与 0.5s 爆发窗对齐存在 ±1s 模糊,净增值只取量级。
这印证了 XNU 源码里的刻意设计:压缩线程 thread_bind_cluster_type(self, 'E', true) SRC(vm_pageout.c:4332)——软绑定 E 簇,注释明说"E 簇不可用时才允许上 P 核"。含义:回收绝不去抢前台的 P 核。若当时相机在前台跑 ISP 管线,压缩爆发一颗性能核都不会占用。
| 层 | 源码事实(xnu-7195.141.2) | 为什么快 |
|---|---|---|
| 线程模型 | vm_pageout_internal_start(vm_pageout.c:5065):iOS 压缩线程数 1,__AMP__+ebound 默认提到 2 个;kernel_thread_start_priority(..., BASEPRI_VM=91)(sched.h:149;用户前台 QoS 最高 47) | 优先级 91 = 抢占一切用户态,有活立刻跑;2 线程并行度随积压自然伸缩(第二个线程仅当积压≥一批才唤醒) |
| 绑核 | thread_bind_cluster_type(self,'E',true)(:4332)+ TH_OPT_VMPRIV(:4311) | E 簇专用+内核资源记账豁免(防自我压缩死锁);不与前台抢 P 核 |
| 流水线 | pgo_maxlaundry=1024 页;每批取 128 页后立刻释放队列锁再压缩(:3936–3986);压缩完 32 页批量归还 free 池(MAX_FREE_BATCH:3909);扫描器 vm_pageout_cluster(:717–744)挂 pgo_pending+thread_wakeup | 扫描器与压缩机经批队列解耦,锁竞争摊薄;纯事件驱动(空闲时 assert_wait) |
| 算法本体 | arm64 默认 CMODE_HYB 混合编码(vm_compressor_algorithms.c:441–447):先 WKdm,失败或产物≥12288B 改试 LZ4 择优(:296–405) | 难压页有兜底;16K 页一个 codec 决策开销极小 |
| WKdm 实现 | 手写汇编 WKdmCompress_16k.s:主分类循环标量整数指令(字典查找天然串行);tag 打包段 NEON 向量化(ld1.2s 一次 16 个 tag+位操作);MZV 单值页字典零初始化顺手检出→4 字节编码;早退检查点 CHKPT_BYTES=416/SHRUNK=426(处理 416B 后产物超 426B 即放弃) | 90 年代字典码+现代工程打磨:单核 ~780MB/s;不在难压页上浪费周期;零页近零成本 |
| 触发 | 水位驱动:free<free_min 唤醒扫描器(vm_resident.c:3351);free_target 公式封顶 31MB(iOS 上限 2000 页) | 持续提前量:相机启动前 100s 内背景压缩已完成 1.5GB,窗口期只剩边缘回收 |
| 因素 | 贡献 | 说明 |
|---|---|---|
| WKdm 算法设计 | ★★☆ | 90 年代压缩缓存研究一系的字典码,本身并不先进,比 LZ4 还简单 GEN |
| Apple 实现质量 | ★★★ | 手写 asm+NEON 打包+MZV+早退+LZ4 择优——这层让单核跑到 ~780MB/s |
| A14 硬件 | ★★☆ | E 核即有 ~780MB/s(16KB 工作集全程 L1 驻留);该量级任何现代手机 SoC 都具备 GEN |
| 系统架构 | ★★★★★ | 专用线程(优先级 91/绑 E 簇)+需求不在回收路径串行(§8)+提前量+零成本文件页逐出垫底(§9) |
| 维度 | iOS 14.8 | Linux 5.10/6.1 |
|---|---|---|
| 单核压缩算力 | ~780MB/s(E 核实测)EXP | lz4 同量级或更快 GEN |
| 压缩执行位置 | 专用内核线程×2,优先级 91,绑 E 簇 | 直接回收在申请者上下文同步执行(vmscan.c:3233 try_to_free_pages);zram 在 bio 提交者上下文压缩(zram.c:1627→1342→1366)——kswapd 写 swap 时 kswapd 压,App 直接回收时 App 自己的线程压 |
| 后台回收线程 | 2 个高优先级专用线程 | kswapd 每 NUMA 节点 1 个(vmscan.c:4043 kthread_run),普通优先级、不绑簇 |
| 匿名页可回收性 | 压缩机内建,无条件 | 必须配置 swap/zram/zswap(vmscan.c:2250:!may_swap || nr_swap_pages<=0 则匿名页完全不回收) |
| 同值页检测 | MZV:字典初始化顺手检出,4B 编码 | page_same_filled:线性扫描全页(zram.c:210–226) |
§2b 说"IOKit bulk-wire 预装配",追问:free 都不足 100MB,如何批量锁页?不还是要先回收吗?要回收——但关键不是"要不要回收",而是回收工作在谁的线程里跑。不是"先回收、再锁页"的串行,而是"锁页照常进行、回收在别的核上并行补货"的生产者-消费者模型。
IOKit wire 路径(vm_object_iopl_request,vm_pageout.c:6036–6067)拿页只有三步:vm_page_grab_options() 从 free 池拿一页 → 拿不到则 VM_PAGE_WAIT() 睡觉 → 被叫醒后重试同一 offset。展开后的 vm_page_wait(vm_resident.c:3813)逻辑:特权线程 free>0 从不等;水位够直接走;否则登记等待者+唤醒回收 daemon+assert_wait 睡觉——没有一行代码去做扫描/逐出/压缩。
图 8-1 XNU 锁页×回收的生产者-消费者结构(源码行号锚点)。消费者遇缺货的唯一动作是叫醒生产者并睡觉;三批生产者(scan+压缩机×2)在 E 簇并行补货。
防死锁设计:free 池底部的保留池只留给回收机器自己(vm_resident.c:3175:vm_page_free_count < vm_page_free_reserved && !(TH_OPT_VMPRIV) 则拒绝分配)——保证生产者永远能拿到页继续干活(压缩本身也要临时占页)。
图 8-2 场景二相机窗口放大(+0→+2.3s)。第一个 250ms 窗口:wire 爆发 1897MB/s 的同一窗口,供给(文件页逐出 1324+压缩 682)= 2006MB/s 同步开动;free 探底 44MB 后在 wire 持续爬坡阶段回升到 126MB——供给侧追平并反超消耗。
| 缓冲层 | 机制 | 本轮实证 |
|---|---|---|
| ①提前量(最大) | 五 App 启动的 100s 里,每次 free 下探都唤醒 scan daemon 干到 free_target 才停——最慢的活(压缩后台 App)在相机开火前就干完了 | 相机启动前压缩机已背景吸收 1.5GB;freeze 又落盘 154MB;相机窗口只剩边缘压缩 474MB |
| ②最大供给不走压缩 | 文件页逐出是 clean 页零 I/O 队列操作(§9 详析) | 883MB/2.3s ≈ 380MB/s,由 scan 线程直接完成 |
| ③爆发速率富余 | 压缩机爆发能力 ~1GB/s(§7),2 线程天花板 1.5GB/s | 窗口期实际只需 682MB/s 峰值——余量 2–3 倍 |
失败模式是"慢"不是"崩"(本轮均未发生,水位 23% 未进 warn 带):锁页线程 vm_page_wait 睡觉 → 代价是相机启动变慢(不是失败)→ 优先级 91 的生产者全速补货 → 仍不够 → jetsam 分级杀后台兜底(零触发)。
§8 指出最大一笔供给(883MB)来自文件页逐出且零 I/O。先修正一个因果:零 I/O 文件页回收的价值不在"快"本身,而在它是唯一能跟上锁页消耗速度的供给侧路径——这也是 XNU 把它放在策略最高优先级的原因。释放一个 clean 文件页,两边都是"摘链表+unmap+归还 free"的纯内存操作,原语层无差异;差异全在策略层。
图 9-1 XNU 受害者选择瀑布(vps_choose_victim_page,vm_pageout.c:2331)与三重文件页偏置。右侧为 vm_pageout.c:2145 注释原文——压缩器积压时扫描器不等它,继续偷文件页:两条供给线永不串行。
| 设计点 | XNU 实现(源码锚点) | 含义 |
|---|---|---|
| 队列分类学 | 按成本物理分队列:cleaned(刚洗净)/ speculative 年龄队列(预读从未触碰,LRU 之外,目标上限 5% 内存)/ inactive external(文件页)/ anonymous(匿名页)——对照 Linux 的 4 条 LRU 双挂牌 | 回收投机型页=弹队列节点:无 pmap 引用检查、无 unmap、无 TLB flush |
| 固定成本瀑布 | cleaned → speculative → background → 文件页 ↔ 匿名页,裁决全偏文件页:ANONS_GRABBED_LIMIT=2(:1462,连拿 2 个匿名页必须回头);filecache_min 地板 ≈可用内存 14%(:2030–2066,divisor=70);jetsam 反向覆盖(:2436–2461,文件缓存远超临界值时改拿文件页) | 确定性策略:内存告急时行为完全可预测(文件页先扛);匿名页是配给制 |
| 死文件快车道 | 引用位命中但 vnode_pager_get_isinuse==FALSE(文件已无人打开)→ 只 re-deactivate 不给二次机会(:3412–3420);对象级整体逐出 100 页/轮(:1966+) | 换掉的 dylib/读完的文档缓存页不会被一次引用位误判救活;死对象缓存批量消失 |
| clean/dirty 分流 | pmap_disconnect_options 原子取 refmod;clean 非 precious → 立即挂 local_freeq 本地批,delayed_unlock 摊销锁开销,vm_page_free_list 批量归还(:3531–3652, :3246) | 全程零 I/O、零压缩、零新分配——本质是内存操作速度 |
| 脏页清洗节流 | external iothread 默认 THROTTLE_LEVEL_PAGEOUT_THROTTLED(后台 I/O 优先级,:4255–4283),仅压力期解除(:2273/:2699) | 平时清洗脏页不抢前台磁盘带宽 |
| free 目标刻意极小 | free_target=15+free/100,iOS 封顶 2000 页=31MB(:211–240) | 系统设计上不维持大 free 池——弹性缓冲就是文件缓存+投机型页;实测 S2 基线 free=48MB 恰好贴 target 上方运行 |
| 设计点 | Linux 实现(5.10/6.1 源码锚点) | 与 XNU 的差异 |
|---|---|---|
| 结构 | 4 条 LRU(active/inactive × anon/file);预读页直接折叠进 inactive file LRU | 无独立投机型队列——回收预读页也走完整 LRU 摘除路径 |
| 二次机会 | page_check_references(vmscan.c:986–1047):referenced_ptes → SetPageReferenced+KEEP;二次 → ACTIVATE;VM_EXEC 文件页首次使用即激活(XNU 无此规则);referenced_page 且非 swapbacked → RECLAIM_CLEAN | 语义几乎同构;Linux 多 VM_EXEC、少死文件快车道(靠 inode/dentry 老化间接达到,粒度更粗) |
| anon/file 配比 | get_scan_count(:2237+):swappiness × refault 成本反比加权(anon_cost/file_cost),每边至少 1/3 压力;file_is_tiny→全力拿匿名;cache_trim_mode→全力拿文件 | 比例模型 vs 固定瀑布:Linux 追求全局代价最优(哪边 refault 少多压哪边),XNU 追求确定性(永远先拿最便宜的) |
| 逐错保护 | workingset refault 距离(:903/:929,5.10 已有):被逐页留 shadow entry,回读时算 refault 距离,距离小立即激活;6.1 MGLRU(lru_gen_look_around,MIN_LRU_BATCH=64)世代老化加速扫描 | Linux 领先项:XNU 只有引用位粗判,无 refault 距离等价物 |
| 执行位置 | kswapd 每节点 1 线程+直接回收在申请者上下文(:3233);脏页交 per-bdi flusher+cgroup writeback | 与 §7/§8 结论汇合:不是 Linux 释放文件页慢,而是释放文件页的活可能落在 App 自己的调用栈里 |
| 差异 | 内容 | 对相机类场景的影响 |
|---|---|---|
| ①选择哲学 | XNU 确定性瀑布(便宜路径优先+多重偏置钉死边界)vs Linux 比例成本模型(统计驱动全局最优) | 告急时 XNU 行为可预测;Linux 取决于当时 refault 统计 |
| ②快车道覆盖 | XNU 两条 Linux 没有的:投机型页 LRU 外免引用检查、死文件免二次机会+对象级逐出;Linux 一条 XNU 没有的:workingset refault 距离 | XNU 的便宜路径覆盖比例更大;Linux 逐错保护更精确(各有胜负) |
| ③执行位置与 I/O 节流 | XNU 回收永远在专用高优先级线程+脏页清洗默认后台 I/O 优先级;Linux 直接回收把整套选择逻辑跑在申请者线程里 | 最重要的一条:同样的相机场景,默认 Linux 更容易把回收停顿放进 App 关键路径 |