iOS Secluded Memory(隐退内存)详解

XNU 内核源码级分析 — 那块为相机藏起来的 90MB 物理页储备池

主锚点 xnu-7195.141.2(iOS 14.8) 跨 iOS 9 / 10 / 11 / 12 / 13 / 18 验证 全部行号可在源码存档复核 定义 生命周期 谁能用 jetsam 记账 观测
默认池大小
90 MB
>1GB RAM 设备 · vm_resident.c:1443-1447
目标页数
5,760
16KB 页 · secluded_target
特权使用者
3+1
Camera / mediaserverd / cameracaptured + IOKit 通道
存在区间
iOS 10+
iOS 9 无 · iOS 10 引入 · iOS 18 仍在

1. 一句话定义 它是什么

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。

它不是什么:不是物理上 carve-out 的专用 RAM(没有特殊物理属性,不同于 IOKit 连续页或 32 位 DMA 的 lopage 池), 只是挂在专属队列、进出受策略控制的普通物理页。

2. 解决什么问题 设计动机

相机是 iOS 里对内存爆发性与确定性要求最苛刻的场景:从按下快门到取景器出图只有几百毫秒, 而这一瞬间要为 sensor/ISP 管线、图像缓冲区抓取几十上百 MB 物理页。若此时系统内存紧张,普通路径只有两个坏结局:

Apple 的解法不是给 Camera 更高的 jetsam 优先级,而是直接从总账里藏起一块内存: >1GB RAM 的设备默认划出 90MB 不参与普通分配 vm_resident.c:1443-1447。这 90MB 平时"不存在",关键时刻只有相机能瞬间吃掉——相当于给相机留的"私房钱"。

与本项目实测数据的关联

本工作区 report.md 的 40 次冷启动实测恰好构成一组对照:

3. 基本数据结构 vm_page.h

对象定义处含义
vm_page_queue_secludedvm_page.h:1273专属页队列(注释即 "secluded for Camera")
VM_PAGE_ON_SECLUDED_Q = 5vm_page.h:200-202页状态机新增一态,与 free/active/inactive 并列
vm_page_secluded_countvm_page.h:1343池内总页数
vm_page_secluded_count_freevm_page.h:1345池内"空闲"页(不属于任何对象,可即刻分配)
vm_page_secluded_count_inusevm_page.h:1347池内"在用"页 = 干净文件缓存页(可随时丢弃回收)
vm_page_secluded_count_over_targetvm_page.h:1359超出目标值的部分(仅这部分对 jetsam 算"可用")
vm_page_secluded_targetvm_page.h:1406目标页数(90MB ÷ 16KB = 5760 页)
vm_page_free_wanted_secludedvm_page.h:1442特权任务的专属等待队列(等池里有页)
VM_PAGE_GRAB_SECLUDED = 0x10000vm_page.h:1493-1504分配选项位:IOKit 预留对象的抓页通行证

恒等式 secluded_count = count_free + count_inuse 在所有路径以断言维护 vm_resident.c:3405-3407vm_resident.c:3530-3532。

精妙之处:池子里存的不全是空闲页,还包括"可随时丢弃的干净文件缓存页"(count_inuse)—— 所以这 90MB 并不白白闲置:平时替文件缓存干活,一旦相机需要可以立即抢占走。这正是注释里 "reclaimable" 的含义。

4. 池大小与参数 boot-args / 设备树

取值优先级:代码默认 → 设备树 → 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_mb90池大小(MB)。0 = 关闭整条机制
secluded_for_iokit1IOKit 驱动(kIOMemoryUseReserve 对象)能否从池里分配
secluded_for_apps1特权 App(Camera 等)能否从池里分配
secluded_for_filecache2文件缓存能否回填进池(代码按非零即开使用)
secluded_shutoff_mbmax_mem/2"内存大户 dipping"触发线(§6)

另有一个常量 secluded_shutoff_headroom = 150MB,注释标注 "original value from N56"(某 iPhone 硬件代号上用 Peace/munch 压力测试调出的经验值) vm_resident.c:1361-1362vm_resident.c:1476-1492。

5. 生命周期:怎么攒、怎么花 回填 / 消耗 / 倒池

普通空闲页 free list vm_page_free_count 干净文件缓存页 eligible_for_secluded 对象 SECLUDED 池 target 5760 页 = 90MB free 部分 secluded_count_free inuse 部分(文件缓存驻留) secluded_count_inuse 两条路都可被特权分配抢占 Camera(前台) can_use_secluded_mem mediaserverd / cameracaptured 跟用 kern_exec.c:1562-1577 IOKit DMA 缓冲 kIOMemoryUseReserve → MAP_MEM_GRAB_SECLUDED 不看进程名 · IOMemoryDescriptor.cpp:524 jetsam(准备杀 Camera 时) kMemorystatusKilledVMPageShortage 回填① 释放页 · 3648 启动期 3776 同类 回填③ eligible 文件页 vm_page_activate · 4911 回填铁律:池未满 且 num_tasks_can_use == 0 特权分配 vm_page_grab_secluded 3369 UPL/缺页落页 vm_fault 1044 · pageout 5650/8778 溢出(target 以上)回流 active/free · pageout 1759 drain 倒池 3510 整池倒回 free/active 抢救免杀
图 A — secluded 池生命周期(数字均为 xnu-7195.141.2 行号锚点)。相机使用期间所有回填路径关闭,池只出不进;相机退出后重新积累。

回填①②:空闲页来源(启动期 + 运行期)

页被释放时,满足三个条件才进 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 中。

溢出与倒池

6. 谁有资格用:身份是硬编码的 三层特权 + IOKit 旁路

资格在 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 跟随
        }
}
iOS 18 差异(xnu-11215.1.10,kern_exec.c:2034-2057):跟随名单增加了 "cameracaptured";主特权仍然只给 Camera。iOS 10 初版(xnu-3789.1.32:1144-1168)没有 platform_binary 门槛,且名单里还有一个开发用 "testCamera"(以及被 #if 00 禁用的 "camerad")。
execve → set_proc_name() kern_exec.c:1562(iOS14.8) ① Camera could_use → 前台时 can_use(独占主特权) ② mediaserverd / cameracaptured could_also → 仅当已有任务在用时跟用 ③ 其他一切进程(含全部三方 App) 无资格 — 无 entitlement、无 API 可申请 前台转换门 priority ≥ JETSAM_PRIORITY_FG · ms:2335 num_tasks_can_use_secluded_mem task.c:9235-9270 Camera 前台 = 1,切后台立即归 0 仅当 num_tasks > 0 时跟用(task.c:9298-9302) shutoff 旁门:footprint > max_mem/2 且距 limit < 150MB 时临时 dipping(task.c:9304-9319) IOKit 旁路(不看进程名) kIOMemoryUseReserve → MAP_MEM_GRAB_SECLUDED → can_grab_secluded 对象(vm_user.c:2727) 该对象的缺页/UPL 落页全程带 VM_PAGE_GRAB_SECLUDED 通行证
图 B — 特权判定链。三类进程身份(exec 时硬编码)+ IOKit 对象旁路;"内存大户" shutoff 是唯一的例外通道。

could → can 的转换:只有前台相机真正"开闸"

拿到 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 的回填铁律:相机前台 = 池只出不进;相机退后台 = 池重新积累。

"内存大户" shutoff 通道(唯一的例外)

任何任务(不需要 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)。

IOKit 旁路:相机管线的 DMA 缓冲这么拿页

驱动以 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 通行证,与进程身份无关。

7. 分配路径:为什么它是"最后一道保障" grab 时机与取页策略

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)分两级:

等不到页的特权任务在专属等待队列 vm_page_free_wanted_secluded 上睡眠 vm_resident.c:3845-3861,有页入池时被优先唤醒 vm_resident.c:3674-3677。

8. 与 jetsam / 内存压力的记账互动 "藏"的四处体现

  1. 压力等级分母直接扣除: memorystatus_level = available × 100 / (total_pages − vm_page_secluded_count) kern_memorystatus.c:5119-5121vm_pageout.c:4410-4414—— 90MB 对内存压力百分比"不存在",系统按少了 90MB 的总内存来管理,这块储备不会让系统"虚胖"。
  2. 可用页口径只算溢出部分:VM_CHECK_MEMORYSTATUS 宏里 available = 外部可分页页 + free + 仅 over-target 的 secluded 页 + purgeable vm_page.h:1533-1539。池子攒到目标值后多出来的部分重新算可用。
  3. vm_stat 完全看不见它:secluded 页不在 free/active/inactive/wired 任何一栏里 (连启动期的初始 wire count 都显式把它扣掉 vm_resident.c:1181-1186)。 vm_stat(HOST_VM_INFO)没有对应字段——这正是设计目标。
  4. 杀进程前的"抢救":当 jetsam 因 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,池子重新积累,为下次启动做准备。

9. 怎么观测它 sysctl(越狱机)

内核导出了一整套 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 次冷启动实验的内存曲线对齐验证。

10. 版本演化 iOS 9 → 18 逐版验证

版本xnu tagsecluded 状态(实测 grep)
iOS 9xnu-3248.20.55无(vm_resident.c 0 处匹配)
iOS 10xnu-3789.1.32引入
iOS 11xnu-4570.1.46加 platform_binary 门槛、去掉 testCamera(kern_exec.c:1179-1195)
iOS 12xnu-4903.270.47memorystatus 集成:could→can 打标、总账扣除
iOS 13xnu-6153.141.1新增 kill 前 drain 抢救逻辑
iOS 14.8xnu-7195.141.2全套:suppression、shutoff 通道、完整 sysctl(本页主锚点)
iOS 16 / 17xnu-8792 / 10063机制原样保留
iOS 18xnu-11215.1.10仍在;跟随名单加 cameracaptured(kern_exec.c:2034-2057)

六个版本的 kern_exec.c Camera 打标对比(iOS 10 无门槛 + testCamera → iOS 11+ 平台二进制门槛 → iOS 18 扩名单),源码已存档于 skill 的 session-archive 目录,可逐行复核。

11. 常见误解澄清

误解事实
"是物理上保留的专用 RAM 区域"不是。secluded 页是普通物理页,无特殊物理属性;只是挂在专属队列、进出受策略控制
"三方 App 也能通过某 API / entitlement 申请"不能。资格 = 平台二进制 + 进程名硬编码(kern_exec.c),无任何公开途径
"白白浪费 90MB 空闲内存"没有浪费。池内大部分是可随时丢弃的干净文件缓存页(count_inuse),平时替文件缓存干活
"jetsam 可以回收它救急"常规 jetsam 看不见它;只有准备杀相机本身时才 drain(§8.4),且这恰恰是为了不杀相机
"mediaserverd 和 Camera 同等待遇"mediaserverd 是跟随者(could_also):仅当相机已开闸(num_tasks>0)时才能跟用
附录 A · FBDP 代号与 #if 11 说明(点击展开)
附录 B · 全部源码锚点清单(xnu-7195.141.2 为主,点击展开)
文件锚点内容
vm_page.h1273 / 200-202队列定义 + Camera 注释 / 页状态枚举
vm_page.h1341-1375 / 1406 / 1442 / 1493-1504计数器组 / target / 等待队列 / grab 选项位
vm_page.h1533-1539VM_CHECK_MEMORYSTATUS(available 只算 over-target)
vm_resident.c1443-1457默认 90MB + 设备树/boot-arg 覆盖链
vm_resident.c1354-1362 / 1476-1492boot-args 默认值 / shutoff 调参(N56)
vm_resident.c3648-3677 / 3776-3782运行期 / 启动期回填
vm_resident.c4911-4923文件缓存回填(inuse)
vm_resident.c3175-3202 / 3369-3507grab 入口 / vm_page_grab_secluded 取页
vm_resident.c3510-3567 / 3845-3861 / 9774-9815drain / 特权等待 / suppression
vm_pageout.c1759-1806 / 4410-4414溢出回流 / 压力分母扣除
vm_pageout.c5650-5655 / 8778-8783UPL / IOPL 落页带 grab 位
vm_fault.c1044-1049 / 4215-4219can_grab_secluded 对象的缺页路径
kern_exec.c1562-1577身份硬编码(Camera / mediaserverd)
kern_memorystatus.c2335-2342 / 5119-5121 / 5518-5564前台开闸 / 总账扣除 / kill 前抢救
task.c9235-9336can/could 状态机、could_also 语义、shutoff 通道
memory_object.c1891-1922mark_eligible_for_secluded KPI
vm_user.c2727-2744MAP_MEM_GRAB_SECLUDED → can_grab_secluded
vm_unix.c3034-3057sysctl 导出全家桶
IOMemoryDescriptor.cpp524 / 567kIOMemoryUseReserve → 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 专节。