大内存申请实测:自定义应用 1GB→2GB 分阶段实验

需求来源:big_mem_test.md | iPhone 12 Pro(A14 / 6GB / iOS 14.8 / Taurine 越狱 / 16KB 页)| 实验工具:phasedmem.c(本仓库 ios-probe/cli)| 2026-08-30
启动申请 1GB 耗时
259ms
空闲局(3.95 GB/s);满载局 392ms(2.61 GB/s),仅慢 52%
满载局 free 起点
36MB
五 App 满载极限局,2GB 申请零击杀全完成
供给主力(满载局)
压缩器
五 App 1GB+ 匿名页被同步压入,压缩比约 3:1
释放 2GB 耗时
143ms
munlock 95ms(10.3GB/s)+ munmap 48ms(41.8GB/s)
文件页回收硬地板
856MB
cleaned→投机→冷文件页→地板;触底后只压缩匿名(reactivated +11,652)
冷热判别强度
800×
同压力下冷页压缩 22-26% vs 热页 0.03%(AF 位二次机会,reactivated +28k)

0. 实验设计

自定义应用 phasedmem 模拟相机的分阶段内存申请行为:

阶段动作模拟对象
STAGE1启动即申请 1GB = 512MB 匿名 zero-fill + 512MB mlock wiredIOSurface pageable 形态 + 驱动 wired 形态
STAGE24 步 × 256MB(128 匿名 + 128 wired),步间隔 5s → 2GB相机管线扩容
SETTLE静置 30 秒稳态观察
RELEASEmunlock 全部 wired → munmap 全部用户态释放效率

采样:0.5s 粒度整机 vm_stat 全字段 + appscan 进程级 footprint 轨迹;工具自带毫秒级自计时。

1. 场景一:清场后 free=2.83GB

1.1 时间线

t free wired anon comp 事件 0s 181,040 44,780 32,525 81,809 phasedmem 启动(1GB 申请中) 1s 118,941 77,587 61,855 81,806 STAGE1 完成: wired +512MB, anon +460MB, free -966MB 5s 102,568 85,741 70,072 81,802 grow1: wired +127MB 11s 85,563 93,924 78,848 81,302 grow2: wired +128MB 16s 69,150 102,117 86,952 81,294 grow3: wired +128MB 21s 52,830 110,309 95,165 81,283 STAGE2 完成(2GB): wired 共 +1,024MB ✓ 21~50s 52,827 110,311 95,168 81,283 静置 30s: 满载纹丝不动 51s 176,610 44,826 33,195 78,967 释放: wired -1,024MB, free +1,428MB(含静置期回填) 55s 181,626 44,835 25,122 86,190 进程退出: free 回到起点(+9MB 净差)

1.2 进程 footprint 轨迹

t= 0s fp=1025.6MB internal=1024.9MB compressed= 0.0MB t= 28s fp=2050.1MB internal=2048.9MB compressed= 0.0MB ← 全解压驻留 t= 54s fp= 1.1MB internal= 0.9MB compressed= 0.0MB ← 释放干净

1.3 全程账本(首样本 → 释放前)

字段起点终点Δ解读
Pages free181,040181,626+9MB净零——申请的都还回去了
wired down44,78044,835+01GB wired 全额摘除
compressor stored81,80986,190+68MB系统背景噪声(其它 App churn)

2. 场景二:五 App 满载,free=36MB 极限局

前置:拉起 抖音 / 淘宝 / 京东 / 网易云音乐 / bilibili 五 App 至满载(free 跌至 2,310 页 = 36MB)。

2.1 时间线

t free wired anon fb comp 事件 0s 3,047 52,366 151,333 110,454 102,363 phasedmem 启动(五App满载中) 1s 2,195 78,157 125,797 111,241 101,073 STAGE1: wired +402MB ← anon -412MB(五App被压) 6s 2,991 87,331 126,502 100,492 107,868 grow1: comp +58MB 内容继续供货 10s 2,789 95,523 120,479 98,561 121,963 grow2: comp 已 +170MB 16s 2,647 103,815 102,357 99,068 148,213 grow3: comp +370MB 21s 3,493 110,928 91,774 97,180 167,893 STAGE2 完成(2GB): comp +65,530页(+1,048MB内容) 21~50s 2,833 112,311 90,979 97,478 167,069 静置: 满载稳定, comp 不再涨 51s 91,153 46,479 68,672 97,479 124,170 释放: wired -1,029MB, free +1,401MB 55s 90,842 45,459 69,660 97,754 124,143 进程退出

2.2 进程 footprint 轨迹(关键差异!)

t= 0s fp=1025.6MB internal= 959.9MB compressed= 65.0MB t= 10s fp=1281.7MB internal=1106.6MB compressed=174.2MB t= 19s fp=1793.9MB internal=1280.7MB compressed=512.2MB t= 29s fp=2050.1MB internal=1377.9MB compressed=671.0MB ← 33%已被压! 与场景一(全解压)截然不同 t= 54s fp= 1.0MB internal= 0.2MB compressed= 0.7MB
核心发现:满载局里 phasedmem 自己的匿名页也有 671MB(33%)被压进压缩器——系统判定"新申请的冷匿名页"同样可压。wired 部分(1GB)则全程豁免(wired 计数精确 +1,024MB)。

2.3 供给链分析(free 仅 36MB,2GB 从哪来?)

供给来源数量机制
五 App 匿名页压缩−1,277MBvm_pageout 同步压缩供货(场景期间 comp stored +1,048MB 内容,物理占用仅 +340MB → 压缩比 3.1:1)
file-backed 摘除−199MB代码页/缓存页直接丢弃(可从磁盘回读)
free 池消耗−21MB几乎没动——本来就只有 36MB
击杀进程0 个五 App 全部存活,纯页级腾挪

2.4 五 App 终态(实验结束后)

Appfootprint内驻压缩态压缩占比
抖音 Aweme533.8MB1.3MB503.5MB94%
淘宝315.4MB0.8MB286.3MB91%
京东130.8MB2.3MB128.1MB98%
网易云音乐289.3MB157.8MB137.6MB48%
bilibili(番剧版)233.4MB151.5MB81.1MB35%

注:网易云/bili 的内驻偏高,因实验结束后部分页已被访问解压回驻。

3. 申请与回收效率(需求 §内存分析 2/3)

操作场景一(free 2.83GB)场景二(free 36MB)退化幅度
STAGE1 1GB(512a+512w)259ms(3.95GB/s)392ms(2.61GB/s)×1.52
— touch 匿名 512MB137ms(3738MB/s)108ms(4732MB/s)更快*
— touch+wired 512MB231ms(wire 6491MB/s)167ms(wire 7962MB/s)更快*
grow 每步 256MB61~76ms(3.3~4.1GB/s)58~95ms(复测)≈持平
STAGE2 全程(4×256MB)20.3s(含 5s 间隔)20.4s(含 5s 间隔)持平
释放 2048MB144ms143ms完全一致
— munlock 1024MB95ms(10.3GB/s)95ms(10.3GB/s)
— munmap 2048MB49ms(41.8GB/s)48ms(41.8GB/s)

* 场景二 touch 更快的原因:五 App 的页预先已在压缩器中"热身",zero-fill 直接消耗刚释放的 free 页,无需唤醒 pageout 扫描器。STAGE1 总时长变慢在于首秒同步压缩五 App 供货的等待。

回收效率的两层含义:① 用户态释放(munlock+munmap)143ms 无场景差异——纯地址空间操作+wired 摘链;② 系统级回收(场景二供给)是同步的:每步 wired +128MB 都伴随五 App 被压,压缩吞吐 ≈ 1GB/20s ≈ 50MB/s 净(WKdm 算法,§6.3 书中实测 240MB/s 级),不阻塞申请路径(mlock 等待 <100ms/步)。

4. 三个最终问题的回答

Q1:iOS 启动自定义应用会申请多少内存?系统需要供给多少?分别是哪些内存?

应用侧:启动即 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(不可回收、全记账、释放前纹丝不动)。

Q2:申请耗时多长?效率多快?free 不足时如何回收供给?

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 全存活

Q3:各统计值变化与合理性

统计值场景一变化场景二变化合理性判定
Pages free181k→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 含压缩态

5. 文件页回收顺序分析

当 free 不足时,系统回收文件页的顺序是什么?本节用源码(xnu-7195.141.2)+ 两个专门实验回答:文件页由哪些成分组成、每种成分的回收优先级、以及地板保护在哪里生效。

5.1 文件页的组成

成分性质回收成本本机实测占比
投机页(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,不单列
注意 vm_stat 口径:"Pages free" 包含 speculative(host.c:815 free_count = vm_page_free_count + vm_page_speculative_count);"File-backed" = speculative + 活跃文件页 + 冷文件页 + cleaned。所以 speculative 是文件页的子集,也是"可用内存"的隐藏分量。

5.2 源码级回收顺序链(vm_pageout.c victim 选择)

pageout 扫描器 vm_pageout_scan 每轮从多个队列按优先级挑选牺牲页(xnu-7195.141.2 osfmk/vm/vm_pageout.c):

优先级队列源码锚点语义
① 最优先cleaned Qvm_pageout.c:2359-2365脏页已经写回变 clean 的页——I/O 成本已付,直接摘除("Try for a clean-queue inactive page... Pick them up now that they are clean")
② 次优先speculative aged Qvm_pageout.c:2367-2378预读但从未被触摸、且已度过 10×500ms=5 秒老化保护期的投机页——零 I/O 直接丢
background Qvm_pageout.c:2380-2419后台模式(darkwake)下的 external 页(CONFIG_BACKGROUND_QUEUE)
inactive external Qvm_pageout.c:2448+普通冷文件页——但受 filecache_min 地板保护:低于地板时改 reactivate 放回(:2475-2490,计数器 vm_pageout_filecache_min_reactivated
⑤ 兜底inactive internal Qvm_pageout.c:2505+匿名页——送压缩器(不是丢弃)

三个调节器(同一文件):

5.3 实测验证一:瞬时冲击的供给分解

1.5GB 匿名压力在 0.5 秒内施加(工具 anonpress.c,ios-probe/cli),单采样窗口内的供给来源分解:

供给 1,536MB = 真free 池 −259MB (19,498 → 2,913 页) 投机池 −398MB (25,474 → 239 页, 100% 排空! ①②级牺牲品) 冷文件页 −255MB (inactive external 摘除) 压缩其他进程匿名 +624MB (compressor 同步供货, 异步完成)

投机池在单个 0.5s 窗口内从 25,474 页被排空到 239 页(−398MB)——这正是源码里 speculative aged Q 排在 inactive external 之前的实测映照:零成本牺牲品先死。同时 fb 从 120,733 → 79,161(−650MB),其中 398MB 来自投机池、255MB 来自冷文件页。

5.4 实测验证二:阶梯加压下的 fb 硬地板

逐级加压 6 × 512MB(每级稳 7 秒)观察 fb 的阶梯响应——用于区分"fb 不降"是地板保护还是"不再需要回收":

累计压力freespecfile-backedfb 较上级压缩次数(累计Δ)reactivated(Δ)供给主力
512MB92,6333,15682,627+2MB010free 池
1,024MB59,6163,22182,715+1MB054free 池
1,536MB26,6063,26182,795+1MB060free 池
2,048MB3,08062076,659−95MB3,4641,090free 枯竭 → 投机池排空(3,261→620)
2,560MB2,81062854,948−339MB17,03811,652冷文件页大额摘除 + 压缩器启动
3,072MB3,01059854,821−1MB50,01011,741fb 钉死地板,全部靠压缩器

三个决定性信号同时出现:

5.5 与大内存实验的互相印证

实验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 硬地板后全靠压缩器
一句话总结:文件页回收顺序 = cleaned(已写回的脏页)→ speculative(零成本投机页)→ 冷文件页(inactive external)→ 856MB 地板(filecache_min 保护,再往下只压缩匿名页不碰文件页)。脏文件页走特殊路径:先写回进 cleaned 队列插队到最优先。活跃文件页需先老化降级。整条链的设计逻辑:按"回收成本"升序支付——零 I/O 的先死,需要 I/O 的最后死,需要写 I/O 的要预付

6. 物理页的冷热识别与管理(源码 + 实测)

场景二的数据里有一个扎眼的数字:测试用例自身的匿名页有 33%(671MB / 2048MB)被压进了压缩器——footprint 轨迹显示 STAGE1 结束(启动后 0.4 秒!)就已压掉 65MB,2GB 满载时达到 671MB。内核怎么知道哪些页"冷"可以压、哪些页"热"要留着?wired 的 1GB 又为什么纹丝不动?本节用源码 + 专门的冷热判别实验回答。

6.1 机制总览:软件引用位 + arm64 硬件 AF 位的"撤销-收割"模型

XNU 不做 periodic 页表扫描,冷热识别分三层(xnu-7195.141.2):

动作源码锚点效果
① 缺页激活每次缺页把页标记"热":vmp_reference = TRUE,入 active 队列;zero-fill/COW 缺页先进 per-CPU 本地队列(软限 250 页/CPU、硬限 500),免全局锁,攒够再批量并入全局 active 队列vm_fault.c:2999-3175(vm_fault_enqueue_page);vm_resident.c:4850-4934(vm_page_activate,:4927 置引用位);本地队列 vm_fault.c:3085-3130、vm_resident.c:360-362新页一律视为热;被再次缺页的页自动回热
② 老化降级当 inactive+speculative 低于目标(=(active+inactive+spec)/2)时,从 active 队列头取页:撤销 PTE 里的硬件 AF 访问位 + 清软件引用位,降入 inactive 队列vm_pageout.c:2769-2824(vm_page_balance_inactive);:2786-2788 目标公式;:2806-2810 撤 AF(故意不做 TLB flush——接受"远端 TLB 缓存漏掉一次引用"的误差,注释 :2797-2808)只按队列 FIFO 顺序降级,不预测——热的判定交给下一步的硬件反馈
③ 回收时二次机会pageout 扫描器从 inactive 取到候选页时,读软件引用位;若期间被访问过(见下),reactivate 送回 active(reactivations 计数器 +1);确认冷的才回收(匿名→压缩器,文件→丢弃)vm_pageout.c:3359-3410;reactivate 上限 VM_PAGE_REACTIVATE_LIMIT = inactive_target/2(:273,注释"别的 CPU 可能在我们清位和遍历之间访问得更快,所以限制 reactivate 数量")LRU 二次机会:访问过的页自动免责
arm64 的关键技巧——"撤销-收割":老化时内核不记录"谁访问了",而是撤掉 PTE 的 ARM_PTE_AF 位(pmap.c:10555-10556,经 phys_attribute_clear → arm_force_fast_fault)。此后 CPU 再访问该页会触发 access-flag fault,故障处理里完成收割:重置 AF 位并把"被引用"写进软件属性位 PP_ATTR_REFERENCED(pmap.c:10768-10780:read fault + AF 清 → tmplate |= ARM_PTE_AF; pa_set_bits(pa, PP_ATTR_REFERENCED))。VM 层读的 pmap_get_refmod(:10082)就是读这个软件位。这样即使页已驻留、访问不产生 VM 缺页,硬件也能替内核记账——这就是"热页无需自己申报,访问行为本身会留痕"的实现。

6.2 实测:冷热判别双探针实验(hotcold.c)

两个同尺寸进程,唯一区别是触摸后的行为——一个沉睡(冷),一个每秒全量回访(热)——施加同样的外部压力(anonpress 2.5GB),对比压缩结果:

进程行为footprint内驻(internal)被压缩压缩占比
cold 384MB触摸一次后 sleep385.2MB300.5MB84.4~99.0MB22~26%
hot 384MB触摸一次后每秒回访全部 24,576 页385.3MB384.7MB0.0~0.1MB0.03%

两个决定性信号:

6.3 边界发现:老化均衡效应(热页的"粘性")

实验中把热进程 kill -STOP(停止回访)并追加 1GB 压力,预期热页变冷被压——结果没有翻转(热页 55 秒后仍 0.0MB 压缩)。整机队列采样解释了原因:

t=35~77s(稳态): active 140,863 inactive 140,395 free 2,233 reactivated 冻结在 352,140 ↑ inactive+spec(141k) ≈ inactive_target(=140k) → 老化 while 条件不成立 → 停止降级 t=77s STOP+追加1GB: compressor stored 89k→156k(压缩的是积压的旧冷页) active/inactive 几乎不动(140k/139k) → 热页仍停在 active 队列, 回收扫描够不着 t=139s 释放: 一切回吐

机制解释(源码 :2790 的 while 条件):老化只在 inactive+speculative < (active+inactive+spec)/2 时进行。压力稳态下两队列各占一半、恰好达标,老化暂停——active 队列上的页(无论冷热)都安全。回收扫描器只从 inactive/speculative 取页,够不到 active 队列。实测铁证是 reactivated 计数器在 35 秒里纹丝不动(+0):没有降级就没有"降级后回访",二次机会循环整体停摆。

推论:冷热保护是队列位置 + 均衡条件的结构性效果,不是持久的"热"标签。active 队列是 FIFO——只要压力持续加深、队头不断被抽走,任何页(包括刚停访的热页)最终都会轮到降级。本实验 55 秒的追加压力只抽掉队头 67k 页(1GB),没排到热页(它们在早期反复 reactivate 时被刷到了队列较新端)。

6.4 回到大内存实验:为什么恰好是 33%?

观测机制解释
STAGE1 结束(0.4s)就有 65MB 被压触摸 512MB 用时 ~110ms,循环开头触摸的页到结束时已"100ms 未回访";free=36MB 的极限局里 pageout 扫描器全程全速运转(每轮 vm_page_balance_inactive(1),vm_pageout.c:2950),老化-压缩流水线跟得上触摸速度——头部页在工具还在跑的时候就被压了
2GB 满载时 671MB(33%)被压phasedmem 的触摸模式是 touch-once(每页写一次后永不回访)——正是 6.2 里 cold 探针的行为。被压掉的 671MB = 触摸较早、已老化降级的页;存活的 1,377.9MB = 触摸较新 + 6.3 的均衡保护(增长步之间的 5s 间隔让 active:inactive 达到 1:1 后老化暂停)
wired 1GB 全程零压缩vm_page_activate/deactivate 都有 VM_PAGE_WIRED 早退分支(vm_resident.c:4741-4747)——mlock 的页根本不进老化游戏,wire_count 是比"热"更强的豁免
场景一(free 2.83GB)同样 touch-once 却 0% 被压free 充足时 pageout 扫描器不运转(vm_pageout.c:207-213 注释:free 高于 target 时 pageout 不启动)——老化降级从未发生,touch-once 的页全部留在 active 队列。冷热识别本身依赖压力:没有回收需求就没有冷热判定

6.5 管理动作全景(谁在哪些队列间搬页)

事件队列迁移记账变化
缺页(zero-fill/COW)→ 本地队列 → activeTranslation faults/zero_fill/cow +1,vmp_reference=TRUE
再次访问(含无 VM 缺页的 TLB 命中后 AF 收割)inactive → active(reactivate)reactivated +1
老化(inactive 不足半数时)active 头 → inactive 尾撤销 AF + 清引用位(无计数器,纯状态)
回收判定:冷匿名页inactive → 压缩器 c_segmentcompressions +1,anonymous −1,compressor stored +1
回收判定:冷 clean 文件页inactive → free无(见 §5 顺序链)
回收判定:冷 dirty 文件页inactive → pageout 写回 → cleaned → 摘除pageouts +1
解压回访(访问已压缩页)压缩器 → activedecompressions +1

一句话:iOS 的冷热管理 = "缺页即热 + 硬件 AF 位留痕 + 老化降级 + 回收前二次机会"的 LRU 近似——不做页表周期扫描、不预测访问模式,把"谁热"的判定交给硬件访问位在两次检查之间的自然积累;再叠一层 inactive 目标均衡,让稳态下的页获得结构性保护。touch-once 工作负载(本实验)恰好是这个机制的最差情况:触摸即热的待遇只有一瞬,之后就按冷页处理。

6.6 附录:XNU 老化机制 vs Linux LRU(源码对照)

核心思想同源——两级 LRU + 硬件访问位 + 二次机会(Linux 2.6 借鉴自 Solaris/BSD 系,XNU 继承 Mach/BSD 血统)——但实现哲学几乎每个环节都分叉。对照基线:XNU xnu-7195.141.2 vs Linux v5.10(含 v6.1 MGLRU 演化)。

维度XNU(iOS)Linux
引用位收割故障时收割:老化撤 PTE 的 AF 位(故意不做 TLB flush,vm_pageout.c:2806-2810 注释明说接受远端 TLB 缓存漏记一次引用的误差)→ 后续访问触发 access-flag fault → 故障处理里写进 per-page 软件位 PP_ATTR_REFERENCED(pmap.c:10768-10780)回收时收割:page_referenced() 走反向映射(rmap)找到该页全部 PTE,逐个 ptep_clear_flush_young_notify 读+清 young 位(rmap.c:850、:767 page_referenced_one),带 TLB flush
回收时单页成本O(1)——读一个软件位(vm_pageout.c:3359-3365)O(映射数)——每次候选判定都要 rmap 遍历;多映射页(共享库)代价高
代价转嫁摊到每次访问的故障路径(降级后首次访问付一次轻量故障,微秒级)摊到每次回收扫描(TLB flush + 遍历)
均衡目标固定 50%:(active+inactive+spec)×1/2(vm_pageout.c:208、:2786-2788);到达即老化暂停(本报告实测:reactivated 冻结 35s +0)容量自适应 inactive_ratio 查表(vmscan.c:2187-2219):1GB→3:1、10GB→10:1、100GB→31:1,int_sqrt(10×GB);6GB 设备 ≈ 7:1,inactive 仅 ~12.5%
缓冲池含义6GB 机型的二次机会窗口约为 Linux 的 4 倍——iOS 宁可少备牺牲品也要防误杀(压缩机兜底使回收错误便宜)内存越大观察窗口占比越小——swap 昂贵(毫秒级 I/O),宁可小窗口也要多备可回收页
冷页去向内存压缩机(一等公民 pager):WKdm 774-793MB/s、压缩比 2-3:1、无 swapfile;回收错误 = 解压成本(微秒级)→ 老化-压缩敢全速跑(实测 50,010 次/秒级窗口)swap 块设备 I/O(zram/zswap 为可选用途):错误 = 磁盘往返(毫秒级)→ 需要额外谨慎机制
工作集理论无对应物——只有每次调用内的 reactivate 上限 VM_PAGE_REACTIVATE_LIMIT(vm_pageout.c:273)workingset_refault:驱逐历史 shadow entry 算 refault 距离估计工作集大小(vmscan.c:903、:929 附近),动态调整扫描倾向
队列生态特化队列群:speculative 投机池(10 bin × 500ms)、cleaned(预付写回插队)、throttled(无 swap 脏匿名限流)、background(darkwake)、per-CPU 本地队列(zero-fill/COW 免锁 250/500 页)每 lruvec 四条通用链(file/anon × active/inactive)+ memcg 隔离 + unevictable 链(mlock 页留在 LRU 上但跳过回收;XNU wired 页彻底出队 q_state=WIRED)
批量优化per-CPU 本地队列蓄积激活(vm_fault.c:3085-3130)pagevec 蓄积 LRU 入队(同目不同层)
演化方向iOS 稳定迭代(speculative 池、cleaned 队列等持续微调)v6.1 MGLRU(CONFIG_LRU_GEN,v6.1 vmscan 中 207 处引用):放弃两级链表改代数编号 + 按 mm 批量扫页表清 young 位(range 级 PTE 扫描代替逐页 rmap)——与 XNU"故障时收割、回收时零遍历"是同一经济压力下的两种收敛解
一句话:同一个问题(不可预知的未来访问)催生了同一个骨架(两级 LRU + 硬件位 + 二次机会),但 XNU 为"无 swap + 有压缩机 + 移动端单用户"定制——收割进故障路径、50% 大缓冲池、特化队列、wired 彻底出队;Linux 为"通用 + swap + 多租户"定制——rmap 遍历、容量自适应比例、workingset 理论、memcg 隔离。前者像专用赛道车(简、快、假设少),后者像越野车(重、通用、可调)。Android(Pixel 的 zram+PSI+MGLRU 组合)与 iOS 的对决里,两边方案正在互相收敛。
实测旁证(本报告 §6.2):热探针每秒回访 24,576 页 × 55s ≈ 135 万次访问,Translation faults 只增 ~20 万——大量回访走的是 AF 故障收割路径(软故障,非完整 VM 缺页),与"收割摊在访问路径上"的源码设计一致。

7. wired 比 anon 快?——申请速度差异的源码归因与分解实验

回看 phasedmem 的数据:wire 阶段 6,491~7,962 MB/s,而 anon touch 只有 3,013~4,732 MB/s——wired 快了近一倍。这个差异是真的,但"为什么"值得拆开。新工具 wireperf.c 用三种模式把"缺页/清零/wire 记账"三个成本正交分离(256MB,三轮中位):

模式操作序列耗时吞吐内核路径
A. anonmmap → touch50ms5,100 MB/svm_fault 全路径:分配页 + zero-fill(bzero_phys 清零 16KB) + pmap_enter + 入 active 队列
B. premmapmmap → touch → mlock50ms + 27ms9,500 MB/s(wire 段)mlock 对已驻留页:vm_fault_wire_fast 快路径——只做摘队 + wire 记账 + PTE 改写,零物理清零
C. wirefirstmmap → mlock → touch47ms + 1ms5,450 MB/smlock 对 absent 页:wire_fast 查不到页 GIVE_UP → vm_fault_internal 全路径(分配+清零+wire+PTE 一次做完);之后 touch 零成本(全驻留,~270GB/s)
已驻留页 mlock 吞吐
9,500 MB/s
vs anon touch 5,100 —— 1.86×,这就是"wire 快"的本体
absent 页 mlock
5,450 ≈ 5,100
都要付分配+清零的钱,wire 只多 7% 记账开销

7.1 为什么 wire 对已驻留页这么快:vm_fault_wire_fast 快路径

源码(xnu-7195.141.2)里 mlock 的调用链是 vm_map_wire → vm_fault_wire(逐页循环)→ 每页先试 vm_fault_wire_fast(vm_fault.c:6045-6052)。快路径只有四步(vm_fault.c:6278-6460):

① vm_page_lookup(object, offset) — 对象内哈希查页 ② vm_page_wire(m) — 摘出队列, q_state=WIRED, wired_page_count++ ③ m->vmp_busy = TRUE — 占位 ④ vm_fault_enter(...) — pmap_enter + 记账, 直接以 wired=TRUE 进入 查不到页/busy/copy 进行中 → GIVE_UP → 走 vm_fault_internal 全路径(慢速兜底)

它跳过的东西(普通缺页 vm_fault 约 3,500 行 vs wire_fast 约 200 行):① zero-fill 的物理清零——普通匿名缺页必经 vm_fault_zero_page → vm_page_zero_fill → pmap_zero_page → bzero_phys(16KB)(vm_fault.c:806-864、vm_resident.c:5357-5369、pmap.c:11042-11047),每页 16KB 的内存带宽是匿名分配的最大单项成本;② 复杂映射链遍历(shadow/copy 对象链);③ zero-fill 页的 per-CPU 本地队列入队(vm_fault.c:3085-3130)。wire 对已驻留页是纯元数据操作——队列摘除 + 计数器 + PTE 属性位,全部 cache-line 级工作。

7.2 但"从零分配 1GB wired"并不比 anon 快

模式 C 证明:对未驻留页,mlock 退回全路径,分配 + 清零的钱一分不少(5,450 vs 5,100 MB/s,wire 记账只多 7%)。phasedmem 里 driver-wired 的计时(touch 152ms + wire 79ms)正是"模式 A + 模式 B"的串联:touch 阶段付清零的钱(和 anon 同价),wire 阶段只付记账的钱(所以单独看 6-8GB/s)。用户感知的"wired 快很多"来自把 wire 段吞吐与含清零的 anon 吞吐直接对比——同口径对比应该是:

同口径对比数字结论
纯 wire 记账(页已驻留)vs 纯匿名缺页9,500 vs 5,100 MB/swire 快 1.86×(无清零成本)
完整分配 1GB(含驻留形成)anon vs wired场景二复测:108ms vs 103+64ms(合计 167ms)wired 反而慢 55%——多一道 mlock 通行
释放:munlock vs munmap10.3 GB/s vs 41.8 GB/smunmap 更快(批量 TLB 击落 vs 逐页记账)

7.3 为什么内核把"清零"做得这么贵却不优化

zero-fill 的 bzero_phys 无法跳过——这是安全语义:匿名页必须保证不泄露上一个使用者的数据(内核页缓存、其它进程的堆)。wire_fast 敢跳过清零是因为它只处理已经驻留且内容已就绪的页(此前那次驻留已经付过清零的钱)。所以"wire 快"的本质是账单分期:把清零成本留在 touch 阶段(或此前的驻留),wire 阶段只剩元数据操作。真正的设计收益在另一个方向:wired 页从此退出所有队列游戏(不老化、不压缩、不进回收候选——vm_resident.c:4741-4747 的 VM_PAGE_WIRED 早退分支),这是拿运行时灵活性换确定性延迟。

8. 实验过程记录(诚实数据)