XNU 内核源码级分析 — 那块为相机藏起来的 90MB 物理页储备池
Secluded memory 是 XNU 内核在普通 free page list 之外单独维护的一个物理页储备池
(队列名就叫 vm_page_queue_secluded),它在 iOS 上的唯一用途是为相机(Camera)预留应急内存。
源码注释写得非常直白:
vm_page_queue_head_t vm_page_queue_secluded; /* reclaimable pages secluded for Camera */ — vm_page.h:1273(xnu-7195.141.2)
"secluded"(隐退、与世隔绝)的含义就是:这些页从普通 VM 的分配、回收、jetsam 统计视野里"隔离隐退"出去——
系统其他部分既看不到它、也拿不到它,只有内核点名授权的极少数特权分配者才能动用。
页状态机为它单设一态 VM_PAGE_ON_SECLUDED_Q = 5,与 free / active / inactive 并列
vm_page.h:200-202。
相机是 iOS 里对内存爆发性与确定性要求最苛刻的场景:从按下快门到取景器出图只有几百毫秒, 而这一瞬间要为 sensor/ISP 管线、图像缓冲区抓取几十上百 MB 物理页。若此时系统内存紧张,普通路径只有两个坏结局:
Apple 的解法不是给 Camera 更高的 jetsam 优先级,而是直接从总账里藏起一块内存:
>1GB RAM 的设备默认划出 90MB 不参与普通分配
vm_resident.c:1443-1447。这 90MB 平时"不存在",关键时刻只有相机能瞬间吃掉——相当于给相机留的"私房钱"。
本工作区 report.md 的 40 次冷启动实测恰好构成一组对照:
vm_stat 快照里完全找不到这 90MB——不是不存在,是被设计成看不见(§8、§9)。| 对象 | 定义处 | 含义 |
|---|---|---|
| vm_page_queue_secluded | vm_page.h:1273 | 专属页队列(注释即 "secluded for Camera") |
| VM_PAGE_ON_SECLUDED_Q = 5 | vm_page.h:200-202 | 页状态机新增一态,与 free/active/inactive 并列 |
| vm_page_secluded_count | vm_page.h:1343 | 池内总页数 |
| vm_page_secluded_count_free | vm_page.h:1345 | 池内"空闲"页(不属于任何对象,可即刻分配) |
| vm_page_secluded_count_inuse | vm_page.h:1347 | 池内"在用"页 = 干净文件缓存页(可随时丢弃回收) |
| vm_page_secluded_count_over_target | vm_page.h:1359 | 超出目标值的部分(仅这部分对 jetsam 算"可用") |
| vm_page_secluded_target | vm_page.h:1406 | 目标页数(90MB ÷ 16KB = 5760 页) |
| vm_page_free_wanted_secluded | vm_page.h:1442 | 特权任务的专属等待队列(等池里有页) |
| VM_PAGE_GRAB_SECLUDED = 0x10000 | vm_page.h:1493-1504 | 分配选项位:IOKit 预留对象的抓页通行证 |
恒等式 secluded_count = count_free + count_inuse 在所有路径以断言维护
vm_resident.c:3405-3407vm_resident.c:3530-3532。
count_inuse)——
所以这 90MB 并不白白闲置:平时替文件缓存干活,一旦相机需要可以立即抢占走。这正是注释里 "reclaimable" 的含义。取值优先级:代码默认 → 设备树 → boot-arg,后者依次覆盖 vm_resident.c:1437-1457:
/* vm_resident.c:1443-1457 (xnu-7195.141.2) */
secluded_mem_mb = 0;
if (max_mem > 1 * 1024 * 1024 * 1024) {
secluded_mem_mb = 90; /* 默认 90MB */
}
PE_get_default("kern.secluded_mem_mb", &secluded_mem_mb, ...); /* 设备树 */
PE_parse_boot_argn("secluded_mem_mb", &secluded_mem_mb, ...); /* boot-arg */
vm_page_secluded_target = secluded_mem_mb * 1024 * 1024 / PAGE_SIZE;
| 参数 | 默认 | 作用 |
|---|---|---|
| secluded_mem_mb | 90 | 池大小(MB)。0 = 关闭整条机制 |
| secluded_for_iokit | 1 | IOKit 驱动(kIOMemoryUseReserve 对象)能否从池里分配 |
| secluded_for_apps | 1 | 特权 App(Camera 等)能否从池里分配 |
| secluded_for_filecache | 2 | 文件缓存能否回填进池(代码按非零即开使用) |
| secluded_shutoff_mb | max_mem/2 | "内存大户 dipping"触发线(§6) |
另有一个常量 secluded_shutoff_headroom = 150MB,注释标注
"original value from N56"(某 iPhone 硬件代号上用 Peace/munch 压力测试调出的经验值)
vm_resident.c:1361-1362vm_resident.c:1476-1492。
页被释放时,满足三个条件才进 secluded 队列而不是普通 free list vm_resident.c:3648-3677(启动早期版本更简单 vm_resident.c:3776-3782):
/* vm_resident.c:3648-3669 */
} else if (vm_page_free_count > vm_page_free_reserved && // free list 高于保留水位
vm_page_secluded_count < vm_page_secluded_target && // 池还没攒满
num_tasks_can_use_secluded_mem == 0) { // 且没有特权任务在用
vm_page_queue_enter_first(&vm_page_queue_secluded, mem, vmp_pageq);
mem->vmp_q_state = VM_PAGE_ON_SECLUDED_Q;
vm_page_secluded_count++;
vm_page_secluded_count_free++;
被标记 eligible_for_secluded 的文件对象的干净页,在 vm_page_activate() 时挂上 secluded 队列、记入 inuse
vm_resident.c:4911-4923:
/* vm_resident.c:4911-4923 */
if (secluded_for_filecache &&
vm_page_secluded_target != 0 &&
num_tasks_can_use_secluded_mem == 0 &&
m_object->eligible_for_secluded) {
vm_page_queue_enter(&vm_page_queue_secluded, m, vmp_pageq);
m->vmp_q_state = VM_PAGE_ON_SECLUDED_Q;
vm_page_secluded_count_inuse++; // 记为“在用”的干净文件页
assert(!m_object->internal); // 只收外部(文件)对象
}
标记接口是内核 KPI memory_object_mark_eligible_for_secluded()
memory_object.c:1891-1922——开源 XNU 树里没有调用方,调用者应在闭源 pager / kext 中。
vm_pageout_scan 每轮把溢出页移回 active 或 free 队列
vm_pageout.c:1759-1806(注释 SECLUDED_AGING_BEFORE_ACTIVE);vm_page_secluded_drain() 把整池倒回 free/active
vm_resident.c:3510-3567(详见 §8)。资格在 execve 收尾处按进程名硬编码授予,且要求是 Apple 平台二进制
kern_exec.c:1562-1577(iOS 14.8):
/* bsd/kern/kern_exec.c:1562-1577 (xnu-7195.141.2) */
if (secluded_for_apps &&
load_result.platform_binary) { // 必须是 Apple 平台二进制
if (strncmp(p->p_name, "Camera", sizeof(p->p_name)) == 0) {
task_set_could_use_secluded_mem(task, TRUE); // 只有 Camera
} else {
task_set_could_use_secluded_mem(task, FALSE);
}
if (strncmp(p->p_name, "mediaserverd", sizeof(p->p_name)) == 0) {
task_set_could_also_use_secluded_mem(task, TRUE); // mediaserverd 跟随
}
}
"cameracaptured";主特权仍然只给 Camera。iOS 10 初版(xnu-3789.1.32:1144-1168)没有
platform_binary 门槛,且名单里还有一个开发用 "testCamera"(以及被 #if 00 禁用的 "camerad")。拿到 could_use 身份还不够,memorystatus 在调整进程优先级时动态开关
kern_memorystatus.c:2335-2342:
/* memorystatus_update_priority_locked() */
if (secluded_for_apps &&
task_could_use_secluded_mem(p->task)) {
task_set_can_use_secluded_mem(
p->task,
(priority >= JETSAM_PRIORITY_FOREGROUND)); // 只有前台才放开
}
can_use 置位时全局计数 num_tasks_can_use_secluded_mem + 1,切后台立即 −1
task.c:9235-9270。这个计数直接控制 §5 的回填铁律:相机前台 = 池只出不进;相机退后台 = 池重新积累。
任何任务(不需要 Camera 身份)满足"用得特别多且逼近自身 limit"时,也临时允许动用池子,并触发 suppression:
/* task.c:9310-9318 */
if (is_alloc && secluded_shutoff_trigger != 0) { // trigger 默认 max_mem/2
uint64_t phys_used = get_task_phys_footprint(task);
uint64_t limit = get_task_phys_footprint_limit(task);
if (phys_used > secluded_shutoff_trigger && // 用量已过半内存
limit > secluded_shutoff_trigger &&
phys_used > limit - secluded_shutoff_headroom) { // 距 limit 不足 150MB
start_secluded_suppression(task); // 顺手停止回填
return TRUE;
}
}
suppression 的实现是把 vm_page_secluded_target 置 0(不再回填),直到该任务退出才恢复
vm_resident.c:9774-9815。源码注释:"Allow a really large app to effectively use secluded memory until it exits."
(默认触发线 max_mem/2 来自一次 N56 设备上 Peace/munch 压力测试的调参 vm_resident.c:1476-1492)。
驱动以 kIOMemoryUseReserve 选项创建 IOBufferMemoryDescriptor 时
IOMemoryDescriptor.cpp:524/567,IOKit 会在
mach_make_memory_entry_64 的权限位里加 MAP_MEM_GRAB_SECLUDED
vm_user.c:2727-2744,产出的 VM 对象带 can_grab_secluded 标志;
此后该对象的缺页 vm_fault.c:1044-1049、UPL 落页
vm_pageout.c:5650-5655 / 8778-8783 全程带 VM_PAGE_GRAB_SECLUDED 通行证,与进程身份无关。
vm_page_grab_options() 的入口逻辑决定了 secluded 的角色定位
vm_resident.c:3175-3202:
/* vm_resident.c:3175-3192 */
if ((vm_page_free_count < vm_page_free_reserved) && // free list 已见底(保留水位以下)
!(current_thread()->options & TH_OPT_VMPRIV)) {
/* 普通任务拿不到页了 —— 但特权者还有一条路 */
if (vm_page_secluded_count > 0 &&
((grab_options & VM_PAGE_GRAB_SECLUDED) || // IOKit 预留对象
task_can_use_secluded_mem(current_task(), TRUE))) { // 或特权任务
mem = vm_page_grab_secluded();
secluded 通道只在普通 free list 见底时启用。内存充裕时大家(包括相机)都走普通分配; 只有系统真的挤到普通任务拿不到页的程度,储备才启用——这正是"储备"二字的本义,也解释了为什么实测中 富余内存(≥300MB free)时内存量不影响相机启动速度。
真正取页的 vm_page_grab_secluded()(vm_resident.c:3369-3507)分两级:
grab_success_free);pmap_disconnect 断映射 → 检查 busy/laundry/dirty 状态
(脏页放弃,统计 grab_failure_dirty)→ 干净页 vm_page_free_prepare_object 后交付
(grab_success_other)。等不到页的特权任务在专属等待队列 vm_page_free_wanted_secluded 上睡眠
vm_resident.c:3845-3861,有页入池时被优先唤醒 vm_resident.c:3674-3677。
memorystatus_level = available × 100 / (total_pages − vm_page_secluded_count)
kern_memorystatus.c:5119-5121vm_pageout.c:4410-4414——
90MB 对内存压力百分比"不存在",系统按少了 90MB 的总内存来管理,这块储备不会让系统"虚胖"。VM_CHECK_MEMORYSTATUS 宏里
available = 外部可分页页 + free + 仅 over-target 的 secluded 页 + purgeable
vm_page.h:1533-1539。池子攒到目标值后多出来的部分重新算可用。vm_stat(HOST_VM_INFO)没有对应字段——这正是设计目标。kMemorystatusKilledVMPageShortage 准备杀一个
有 secluded 特权的进程(即相机)时,先 vm_page_secluded_drain() 把整池倒回 free/active
vm_resident.c:3510-3567,倒完压力缓解就不杀了;即使没完全脱离 pressure、只要出了
critical 水位也宣告成功免杀 kern_memorystatus.c:5518-5564:"We're about to kill a process that has access to the secluded pool. Drain that pool into the free or active queues to make these pages re-appear as 'available', which might make us no longer need to kill that process. […] This is an important process and we'd rather not kill it unless absolutely necessary." — kern_memorystatus.c:5522-5560(xnu-7195.141.2)
配套设计:相机使用期间池子只出不进(§5 回填铁律),避免相机与回填逻辑互相打架;
相机退出/被杀后 task_terminate 清标志 task.c:3109-3116,池子重新积累,为下次启动做准备。
内核导出了一整套 sysctl(定义于 vm_unix.c,iOS 14.8 行号见 vm_unix.c:3034-3057):
# 越狱设备(本工作区 iPhone 12 Pro / iOS 14.8.1 / Taurine)直接可读:
sysctl vm.page_secluded_target # 5760(= 90MB / 16KB)
sysctl vm.page_secluded_count # 池内当前总页数
sysctl vm.page_secluded_count_free # 池内空闲页
sysctl vm.page_secluded_count_inuse # 池内文件缓存页
sysctl vm.page_secluded_count_over_target # 超标部分(对 jetsam 可用)
sysctl vm.page_secluded_eligible # eligible 对象的驻留页数
sysctl vm.page_secluded_grab_success_free # 从池内 free 页成交次数
sysctl vm.page_secluded_grab_success_other # 偷文件页成交次数
sysctl vm.page_secluded_grab_for_iokit # IOKit 通道请求次数
sysctl vm.num_tasks_can_use_secluded_mem # 当前有几个特权任务(前台相机=1)
典型动态:相机在前台拍照/录像时 page_secluded_count 逐渐下降(只出不进);
退到后台后缓慢回升(回填重启)。把这条采样并入 probe/ 的 camwatch 时间线,
即可与 40 次冷启动实验的内存曲线对齐验证。
| 版本 | xnu tag | secluded 状态(实测 grep) |
|---|---|---|
| iOS 9 | xnu-3248.20.55 | 无(vm_resident.c 0 处匹配) |
| iOS 10 | xnu-3789.1.32 | 引入 |
| iOS 11 | xnu-4570.1.46 | 加 platform_binary 门槛、去掉 testCamera(kern_exec.c:1179-1195) |
| iOS 12 | xnu-4903.270.47 | memorystatus 集成:could→can 打标、总账扣除 |
| iOS 13 | xnu-6153.141.1 | 新增 kill 前 drain 抢救逻辑 |
| iOS 14.8 | xnu-7195.141.2 | 全套:suppression、shutoff 通道、完整 sysctl(本页主锚点) |
| iOS 16 / 17 | xnu-8792 / 10063 | 机制原样保留 |
| iOS 18 | xnu-11215.1.10 | 仍在;跟随名单加 cameracaptured(kern_exec.c:2034-2057) |
六个版本的 kern_exec.c Camera 打标对比(iOS 10 无门槛 + testCamera → iOS 11+ 平台二进制门槛 → iOS 18 扩名单),源码已存档于 skill 的 session-archive 目录,可逐行复核。
| 误解 | 事实 |
|---|---|
| "是物理上保留的专用 RAM 区域" | 不是。secluded 页是普通物理页,无特殊物理属性;只是挂在专属队列、进出受策略控制 |
| "三方 App 也能通过某 API / entitlement 申请" | 不能。资格 = 平台二进制 + 进程名硬编码(kern_exec.c),无任何公开途径 |
| "白白浪费 90MB 空闲内存" | 没有浪费。池内大部分是可随时丢弃的干净文件缓存页(count_inuse),平时替文件缓存干活 |
| "jetsam 可以回收它救急" | 常规 jetsam 看不见它;只有准备杀相机本身时才 drain(§8.4),且这恰恰是为了不杀相机 |
| "mediaserverd 和 Camera 同等待遇" | mediaserverd 是跟随者(could_also):仅当相机已开闸(num_tasks>0)时才能跟用 |
XXX FBDP TODO 注释和一个 93MB(map_size == 97550336)硬编码测试钩子
(vm_user.c:2730-2739,标注 "for my testing only")表明 FBDP 是某工程师的内部项目代号
(与 file-backed 页面 / 文件缓存工作相关),确切含义未公开;#if 11 / #if 00 是 Apple 工程师保留开发代码的习惯写法(非零恒真 / 零恒假),
本页引用的行号若落在这种块内均属"一直生效"的正式代码;secluded_for_filecache 默认值 2 在 iOS 14 代码里仅按"非零即开"使用,未见 1/2 的语义区分。| 文件 | 锚点 | 内容 |
|---|---|---|
| vm_page.h | 1273 / 200-202 | 队列定义 + Camera 注释 / 页状态枚举 |
| vm_page.h | 1341-1375 / 1406 / 1442 / 1493-1504 | 计数器组 / target / 等待队列 / grab 选项位 |
| vm_page.h | 1533-1539 | VM_CHECK_MEMORYSTATUS(available 只算 over-target) |
| vm_resident.c | 1443-1457 | 默认 90MB + 设备树/boot-arg 覆盖链 |
| vm_resident.c | 1354-1362 / 1476-1492 | boot-args 默认值 / shutoff 调参(N56) |
| vm_resident.c | 3648-3677 / 3776-3782 | 运行期 / 启动期回填 |
| vm_resident.c | 4911-4923 | 文件缓存回填(inuse) |
| vm_resident.c | 3175-3202 / 3369-3507 | grab 入口 / vm_page_grab_secluded 取页 |
| vm_resident.c | 3510-3567 / 3845-3861 / 9774-9815 | drain / 特权等待 / suppression |
| vm_pageout.c | 1759-1806 / 4410-4414 | 溢出回流 / 压力分母扣除 |
| vm_pageout.c | 5650-5655 / 8778-8783 | UPL / IOPL 落页带 grab 位 |
| vm_fault.c | 1044-1049 / 4215-4219 | can_grab_secluded 对象的缺页路径 |
| kern_exec.c | 1562-1577 | 身份硬编码(Camera / mediaserverd) |
| kern_memorystatus.c | 2335-2342 / 5119-5121 / 5518-5564 | 前台开闸 / 总账扣除 / kill 前抢救 |
| task.c | 9235-9336 | can/could 状态机、could_also 语义、shutoff 通道 |
| memory_object.c | 1891-1922 | mark_eligible_for_secluded KPI |
| vm_user.c | 2727-2744 | MAP_MEM_GRAB_SECLUDED → can_grab_secluded |
| vm_unix.c | 3034-3057 | sysctl 导出全家桶 |
| IOMemoryDescriptor.cpp | 524 / 567 | kIOMemoryUseReserve → MAP_MEM_GRAB_SECLUDED |
跨版本 kern_exec.c 存档(iOS 10/11/18)与 iOS 14.8 全套源文件位于
~/.dsh/skills/xnu-source-analysis/session-archive/;iOS 12/13/16 对照件位于其
multi-version-archive/。锚点笔记已同步追加至 anchors-ios14.md secluded 专节。