iOS 越狱真机实测:PageFault 与内存压缩/解压性能多维论证

全部结论仅来自本页引用的设备实测数据(测试用例 + 真实应用)与 XNU 源码锚点,二者互相交叉验证;无经验性断言。

0. 摘要(每条都有下文数据支撑)

  1. 次要缺页(零填充)成本稳定且可预测:16KB 页 ,4MB→256MB 工作集波动 < ,顺序/随机差异 < 4%;ru_minflt 增量与页数在全部 行严格相等(自校验)。
  2. 解压远快于压缩(字节导向算法):libcompression LZ4 在 mixed 数据上解压/压缩吞吐比 ≈ ;内核自研 WKdm 则近似对称( / MiB/s),这是它被选为内核缺页路径编解码器的原因之一。
  3. 内核压缩器是"最后手段"且受速率限制:内存充裕时(free 1.38GB)持续 20s 零压缩;8×384MB 突发压力下 2s 内 free 2.96GB→42MB,压缩器仅处理 (比率 ,与 WKdm 实测 TYPICAL 比率吻合)。
  4. 真实应用大规模依赖压缩器:挂起态支付宝 脏内存位于压缩器内(占其 footprint ),物理驻留仅 。
  5. 受害者选择按"年龄"而非"谁分配":6×320MB 新分配冷页在 1.95GB 全局压缩发生期间无一被压缩(重读 0 缺页);内核优先回收更老的应用冷页 —— 与 vm_pageout_scan 的年龄队列语义一致。
  6. 源码↔真机闭环验证:设备 vm.lz4_threshold=12288 与源码 arm64/16K 分支设定一致;wk/lz4_compressions 计数器恒 0 由 RELEASE 内核 VMCSTATS 编译屏蔽解释(非未使用)。

1. 测试环境与仪器

16KB 页大小的验证链(为何确信"每次缺页 = 16KB"): ① 判别实验(granule_test,本表下方复现附录):在每个 16KB 块内触碰 4 个 4KB 对齐偏移(0/4096/8192/12288),ru_minflt 增量仍 = 块数 8192 而非 4×8192 —— 若页为 4KB,这 4 次触碰会落入 4 个不同页产生 32768 次缺页;触碰"块内偏移"是免费的 ⇒ 缺页粒度 = 16KB(审计 ID-GRAN-1/2/3)。 ② 字节量子整除普查:全部 1568 个 resident / 703 个非零 compressed 内核上报值均为 16384 的整数倍,0 例外(4KB 量子模型下此概率 ≈ (1/4)⁷⁰³;审计 ID-SITE-2)。 ③ 守恒恒等式(§5b 真实数据实验,已持久化原始 JSON):realprobe run3 注入真实应用脏页后,子进程 resident 增量 = compressed 减量 = ru_minflt×16384 = 43,859,968 B(2677 页)三方精确相等、majflt=0;appwake 唤醒美团/discover 挂起进程时 compressed 减量 = 解压页数×16384 逐页精确 —— 每次缺页恰好在压缩器与驻留集之间移动一个 16KB 页。(勘误:本节早期版本引用的 7753×16384 数字未持久化原始数据,已被上述可审计数据替代。) ④ 内核二进制指纹:sysctl vm.lz4_threshold=12288 仅在源码 vm_compressor_algorithms.c L447-453 的 PAGE_SIZE==16384 分支被设定(默认 2048)⇒ 运行内核以 16KB 页编译。 ⑤ 内核自报四口径一致:hw.pagesize / sysconf(_SC_PAGESIZE) / TASK_VM_INFO.page_size / footprint 工具页注均为 16384。 (诚实说明:微基准里 minflt==页数 这条检查本身不能区分 4K/16K —— 16KB 步长触碰在 4KB 页机器上也每步命中新页;它只验证计数器记账。区分靠上面 ①②③。)
所有二进制(memperf/appsamp/decomp_probe*/granule_test)均以 iOS SDK 交叉编译、ldid 注入 platform-application 签名后在设备上原生执行;采样通过 task_for_pid + task_info 与 host_statistics64/sysctl vm.compressor_* 完成,不依赖任何模拟。

2. 维度一:PageFault 微基准(测试用例 TC-PF)

方法:每次重新 mmap(MAP_ANON|MAP_PRIVATE) 后逐页写入 1 字节触发零填充次要缺页(16KB/页),mach_absolute_time() 计时、getrusage 计数,5 次重复取中位数;随机模式用 Fisher–Yates 打乱页序。

论证:① 缺页成本 ≈ 内核陷入 + VM 对象查找 + 16KB 零填充,其中零填充分带宽数量级(),因此 seq/rand 与工作集大小都不敏感 —— 缺页不是局部性问题而是固定成本问题;② 该数字是后续"解压缺页"成本模型的基线(§6)。

3. 维度二:压缩 / 解压吞吐(用户态编解码器 × 内核真实编解码器)

3.1 内核实际使用的编解码器:XNU arm64 WKdm(16K 页版本)

我们将 XNU 源码树中的 WKdmCompress_16k.s / WKdmDecompress_16k.s / WKdmData.s(即内核压缩器在 arm64 16K 页上执行的同一段汇编)直接汇编链接进测试二进制,在真机上测量内核真实路径的性能上限:

论证:WKdm 是字典+重排编码、无熵编码 stage,因此压缩/解压近对称();对 MOSTLY_ZEROS 解压高达 MiB/s —— 单值/稀疏页走 c_segment_sv_hash 单字存储路径(源码 vm_compressor.c L3952-3960),解压近似 memset。RANDOM 页被识别为不可压后原样存储(ratio=1.0,"压缩"吞吐 13857 MiB/s 实为检测+memcpy 成本)。

3.2 用户态 libcompression 矩阵(算法 × 数据形态 × 尺寸)

论证:① 同一算法家族内解压显著快于压缩(LZ4 mixed: MiB/s)——匹配解码无需搜索的事实;② LZFSE/LZMA 换来更差的移动端性价比:mixed 数据上 LZMA 压缩比 LZ4 慢 ×而比率仅持平(≈0.51,该数据集本身混合了不可压段);③ 尺寸 64KB→4MB 吞吐稳定(±3%),块压缩无跨块状态,内核按页压缩亦然。

3.3 zlib 等级权衡

论证:L1→L9 吞吐下降 ,比率仅改善 ;解压速率与等级无关( MiB/s 恒定)—— 等级只改变编码侧搜索深度。移动内存路径上高级别压缩得不偿失。

4. 维度三:内核压缩器何时被触发(多进程压力实验)

方法:fork N 个独立地址空间(绕过 iOS 单进程驻留上限),各自 mmap 并填充高压缩性 text 数据,父进程 2s 步进采样全局压缩器字节/页计数器。

论证:① control(6×288MB):free 下探 1.38GB 后立即稳定,20 秒内 vm.compressor_input_bytes 增量为 0 —— iOS 14.8 在空闲页充裕时完全不压缩,宁可把冷页留在 inactive 队列;② aggressive(8×384MB):2 秒内 free 2.96GB→42MB 触发安全停机,压缩器同期仅消化 (≈46MB/s 的突发处理速率),远追不上 ~1.5GB/s 的子进程缺页填充速度 —— 压缩器是滞后、限速的兜底,不是并发回收通道;③ 实测聚合比率 落在 WKdm TYPICAL 区间(§3.1 ratio ),佐证 bulk 路径主体走 WKdm 系编码。

5. 维度四:真实内核解压路径探测(老化 + 受害者选择)

方法 v1:6 个子进程各映射 320MB text 数据并触碰一次(成为冷脏页),等待压缩器稳定后令其逐页重读(每 16KB 页读 1 字节 = 恰好一次潜在解压缺页),全程读 vm.compressor_decompressions 与 ru_minflt。

论证(受害者选择):压力期内全局压缩了 1949MB,但我们这批"年轻"冷页一页未失(重读 0 缺页、0 解压)—— 内核按年龄优先牺牲更老的应用冷页。推论:应用退后台越久,其内存越先被压缩(与 §6 真实应用观测互证)。v2(75s 老化 + 不可压 wave2 施压,使我们页成为最老冷页)结果见下表,用于测得真实解压缺页成本并与 §6 成本模型对账。

5b. 维度四·二:真实应用数据上的压缩/解压效率(脏页转储 + 内核同源编解码器 + 唤醒式解压)

前文合成数据(TYPICAL/text 填充)回答"算法有多快",本节回答"真实应用的内存有多难压、压/解各多快"。方法链三件套(均为本会话新增仪器):

① appdump2:对真实进程枚举 RW 区域,逐页 mach_vm_page_query,只转储 dirty+present 页(vm_compressor 的真实作用人群)。普查显示真实进程 RW 虚拟地址空间中仅约 4% 被触碰变脏(如 discover:46025 已分配未触碰 / 1801 脏 / 473 已逐出)——"应用申请了很多、真正用到的很少"。
② realcodec:把 XNU 内核压缩器在 arm64/16K 页执行的同一段 WKdm 汇编直接链接运行在转储出来的真实脏页上(全部 memcmp 往返校验)。
③ appwake2:对挂起应用(页已在压缩器内、由内核自己挑选)逐页 mach_vm_read_overwrite 强制内核走真实解压缺页路径(vm_fault → c_decompress_page),每页前后各读一次任务的 compressed 计数器——每次读是解压还是零填充由独立内核计数器逐页标注,非推断。

5b.1 编解码器层:真实脏页 vs 合成数据

论证:合成 TYPICAL 基准(1595/1980 MiB/s,ratio 0.369)把真实应用数据的压缩吞吐高估 2.1×、解压吞吐高估 1.85×(168MB 语料实测 774/1072 MiB/s,ratio 0.501)。真实比率分布在 0.40–0.67:五个常规应用进程 0.40–0.42,发现页(discover,含 14% 不可压块)0.666。单值页仅 2.4%、完全不可压 5.4% —— 真实堆不是"text",是低熵结构 + 指针 + 零星高熵块混合体,逐页比率直方图峰值在 0.3–0.5。结论:用合成数据报告"iOS 内存压缩性能"会系统性失真,必须以真实应用脏页为准。

5b.2 内核真实路径解压:唤醒挂起应用(逐页真值标注)

同一守恒律在注入式实验(realprobe,把 168MB 真实脏页注入自身进程、老化、深压、逐页重读)上独立复核:

论证:对内核自己压缩了的真实应用页(含美团主应用),内核路径解压中位 8.96–10.17 µs/页(p90 ≤ 20.2µs),有效吞吐 889–1422 MiB/s;对照组(跨进程读同样 16KB 冷驻留页,相同 MIG 调用)中位 21.75–22.04 µs —— 解压页因刚被内核写出而缓存热,比冷驻留页读回还快。守恒恒等式逐次精确(dec_bytes = 页数×16384 = compressed 计数器减量,ID-AW-1)。机制:内核选中页比率 0.737(难压子集)时解压仍只有 ~9µs,因为混合编解码器对 WKdm 产出 ≥ 12288B 的页改存 LZ4(vm_compressor_algorithms.c L296-405,lz4_threshold=12288 为 16K 页 arm64 编译期设定 L453),LZ4 解码 ~3-5µs/16KB —— 单算法基准无法预测混合编解码器的真实解压性能。

5b.3 内核真实路径压缩:摄入速率

论证:内核压缩器以突发方式工作——12×384MB 压力窗口 4 秒内摄入 2318MB(≈580MB/s 峰值,含自身 text 段),8×384MB 预消化窗口 227MB(系统最老页池,比率 0.3525 ≈ 真实应用稳态),持续压力下 ~46MB/s(§4 aggressive)。真实应用维度:realprobe 注入的 168MB 真实脏页在 free<200MB 深压下进入压缩器,子进程 compressed 计数以 ≥166MB/s 突发增长,但仅 42MB/168MB 被收纳——且 run5 显示内核宁可 1:1 原样收纳不可压随机页(比率 0.973)也不优先选高可压语料页,受害者选择由队列年龄与逐出路径决定而非"压缩收益",这是注入式测量的固有边界,也正是 appwake2(读内核已压缩页)成为解压测量黄金路径的原因。

5b.4 诚实边界

主应用(微信/淘宝等)无法在前台活跃态转储——实验期间设备锁屏、SpringBoard 拒绝 uiopen 启动(系统应用同样拒启,验证非越狱工具问题);因此 5b.1 语料来自真实应用的 widget/扩展进程(哔哩哔哩×2、淘宝 widget、News 通知服务、系统通知服务、discover)+ 美团与 Weather 主应用的挂起态(5b.2 由 appwake2 直接测其压缩页)。扩展进程与应用主体使用相同框架与堆分配器,其脏页属真实应用内存;但与"前台重度使用"的堆形态可能有差异,特此声明。另:mach_vm_page_query 的 PAGED_OUT 位实测标识的是文件页逐出(external pager)而非压缩器页——本节早期用该位筛选"压缩页"的读法已被计数器守恒法取代。

6. 维度五:真实应用(微信 / 淘宝 / 京东 / 美团 / 支付宝 / Safari)

6.1 后台驻留横断面:谁在被压缩

论证:挂起应用普遍呈现 resident ≪ compressed 的形态(支付宝 resident vs compressed);前台活跃应用(Safari/Preferences)则相反。压缩器实质上承担了挂起应用的二级存储:脏页不落盘(iOS 14.8 无 swap,vm.compressor_swapouts_under_300s=0),全部就地压缩。

6.2 冷启动缺页风暴

论证:冷启动 万次缺页集中在首 秒,峰值速率 (单应用)—— 与微基准单线程 /s 同数量级,说明启动期瓶颈是缺页固定成本 × 页数,而非磁盘 I/O(pageins 远小于 faults)。

6.3 压力下的应用再激活(reactivation)

论证:设备级压力上升时,采样中的前台应用 faults 速率不降,但全局 compressor_decompressions / reactivations 增长加速 —— 被压缩的挂起应用在其(少量)被再次触碰的页上付出解压成本,而前台应用受 vm_pageout 保护。表:挂起应用在 6×384MB 压力(free 下探 348-518MB)期间完全冻结 —— faults Δ=0、压缩字节不变:它们早已被压缩过(稳态即完成),且未被唤醒。

7. 维度六:XNU 源码交叉验证(tag xnu-7195.141.2 = iOS 14.8)

论证闭环:① 混合编解码选择:metacompressor() 先 WKdm,失败或产出 ≥ 阈值再试 LZ4 择优(vm_compressor_algorithms.c L296-405);设备 vm.lz4_threshold 实测 12288 = 源码 arm64 16K 页的编译期设定(L453),配置与二进制一致。② wk/lz4_compressions 恒 0 的"反常":RELEASE 内核 VMCSTATS=(DEVELOPMENT||DEBUG) 把计数语句编译为空(L122-140)—— 计数器不可用 ≠ 算法未使用,不能据此判断 WKdm/LZ4 占比(此前多份分析踩过此坑)。③ 解压缺页路径:vm_compressor.c c_decompress_page L4077 → metadecompressor L4311 分派;每次线程解压计数 vm_fault.c L163(正是本报告 appsamp/decomp_probe 采样的那组计数器)。

8. 综合论证:成本模型与系统设计含义

次要缺页(16KB 零填充)实测成本
WKdm 单页(16KB)解压 @ MiB/s
解压缺页预测成本 = 缺页开销 + 解压
v2 实测解压缺页成本(若触发,见 §5)
系统设计含义(全部由上文数据推出):
① 压缩比换来的内存 ≫ 解压代价:真实系统生命周期比率 (input → compressed ),等效把 6GB 物理内存放大约 ×;解压一页 ~11µs 级,仅相当于 3.7 次零填充缺页 —— 对挂起应用这是纯收益。
② 压缩器不能救突发:~46MB/s 的实测突发消化率(§4)意味着 1GB 突发需要 ~20s 兜底窗口;app 的内存峰值必须靠 jetsam 阈值而非压缩器兜底 —— 与 Apple 对 app 的内存纪律要求一致。
③ 缺页成本恒定 ⇒ 启动优化方向是减少页数(预触摸/按需分页/共享缓存),而非改善局部性(§2 seq/rand 差异 <4%)。
④ 编解码器选型:内核选 WKdm(对称、单页独立、无熵段)契合缺页路径的同步小对象特征;用户态批量大块数据则应选 LZ4 家族(解压 16GB/s 级)。两者在本报告同机同数据上均有实测(§3.1/§3.2)。

9. 数据审计(机械复核)

以下恒等式在原始数据上程序化复核(site/audit.py),任何一条失败即整体判废:

10. 复现附录

设备与工具链

iPhone 12 Pro(D53pAP)· iOS 14.8 (18H17) · Darwin 20.6.0 · A14 6 核 · 6GB · Taurine 越狱 · 16KB 页。
macOS 侧:xcrun -sdk iphoneos clang 交叉编译 + ldid 签名;USB 经 iproxy 2222→22,paramiko 执行。
设备侧工具:/var/mobile/ios_perf/{memperf,appsamp,decomp_probe2}(本仓库 ios-perf-bench/bench/)。

测试用例清单

memperf pagefault:4/16/64/256MB × seq/rand × 5 reps,mmap 冷启动。
granule_test:页粒度判别 —— 每块触碰 1 点 vs 块内 4 点(0/4096/8192/12288)vs 块内 2 点(0/8192),三相对比 ru_minflt;16KB 页下三者缺页数相等,4KB 页下为 1×/4×/2×。
memperf compression:LZ4/LZ4_RAW/LZFSE/LZMA/ZLIB × zero/text/mixed/random × 64KB/256KB/1MB/4MB × 5 reps,往返 memcmp 校验。
memperf wkdm:XNU WKdm 16K 汇编(ALL_ZEROS/MOSTLY_ZEROS/TYPICAL/RANDOM)。
memperf mproc:N 进程 × per_mb × hold,2s 步进全局采样;free<96MB 安全停机。
appsamp:task_info(VM/EVENTS) + host_statistics64 + vm.compressor_* @250-500ms。
appdump2:mach_vm_region_recurse 枚举 RW 区域 + mach_vm_page_query 逐页 disposition,只转储 dirty+present 真实脏页(manifest 含页面形态普查)。
realcodec:内核 WKdm 16K 汇编(WKdmCompress_16k.o/WKdmDecompress_16k.o)直跑真实脏页转储,逐块 memcmp 往返校验 + 比率直方图。
appwake2:对挂起应用逐页 mach_vm_read_overwrite 强制内核解压缺页,每页前后读 TASK_VM_INFO.compressed 逐页归类(解压 vs 零填充),含驻留读对照组。
realprobe:真实脏页语料注入自身进程 → 老化 → 自适应不可压施压(消化窗口)→ SIGUSR1 逐页重读,父进程 250ms 采样子进程 compressed 曲线。
granule_test:页粒度判别(见 §1 验证链)。
decomp_probe v1/v2:冷页老化 + 逐页重读,解压缺页计数。

原始数据文件(本站 data.json 来源)