态本身:不可能;但内容:可能。 处于 NONVOLATILE 状态的对象永远不会被内核选中回收(受害者只从 volatile 队列里挑 SRC vm_purgeable.c:915-995)。但一条 NONVOLATILE 的区域完全可能内容早已丢失——它经历了 VOLATILE → (内核回收) → EMPTY 之后又 SET_STATE(NONVOLATILE):转换照常成功、页面以零填充回归,唯一的损失通知是 SET_STATE 的返回值(旧状态)= VM_PURGABLE_EMPTY(2) SRC vm_object.c:6165。错过这个返回值,之后 GET_STATE 只会告诉你是 NONVOLATILE,丢失事实永远无法再查询。另注意:NONVOLATILE 页仍可能被压缩器移出 DRAM(内容保留、footprint 保留,属正常换页),这与"回收/purge"(内容销毁+零填充)有本质区别。
EMPTY 是 VOLATILE 的子状态,不是并列的独立态。 头文件原文即定义:#define VM_PURGABLE_EMPTY 2 /* purgeable object is volatile and empty */ SRC vm_purgable.h:152——EMPTY = "volatile 且已清空"。三态共用 vm_object 里同一个 2-bit 字段 purgable(0/1/2/3=DENY)SRC vm_object.h:265;记账上 VOLATILE 与 EMPTY 的页都记在 volatile ledger SRC vm_object.c:5664-5666;缺页路径上两者被同一个宏等同对待 SRC vm_object.h:412-414。区别只在内容是否还在:VOLATILE=可丢但还没丢,EMPTY=已经丢了。"volatile 期间是否被回收"正是通过 GET_STATE 返回 1 还是 2 来观测的——状态会在你不知道的时候从 VOLATILE 静默变成 EMPTY。此外 EMPTY 是单向门:请求 SET_STATE(VOLATILE) 会被内核改写回 EMPTY("what's been emptied must stay empty")SRC vm_object.c:5902-5905。
purgeable memory(可清除内存)是 XNU 提供的"应用自愿声明可丢弃"的匿名内存机制:应用分配一块内存(vm_allocate(VM_FLAGS_PURGABLE)),在不用时把它标为 volatile——内核从此有权直接丢弃这些页(不换页、不压缩、无 I/O);应用要用时再标回 nonvolatile,并检查内核是否已经把内容丢了(丢了就得重建)。它是 iOS 上"可再生的缓存"(图片解码缓冲、IOSurface 像素缓冲、字体缓存等)的标准载体。
内核侧的设计目标可以概括为三层:
vm_pageout_scan 的第一顺位回收手段("Before anything…")SRC vm_pageout.c:2999-3006;phys_footprint 摘除 EXP 见 §9 S2——这正是它成为 iOS 应用规避 jetsam 压力的正规手段的原因(不用等真正回收发生)。vm_purgable_control(少一个 e),其余内部符号拼 purgeable。这是历史遗留,vm_purgeable_internal.h:25-35 的注释专门解释了这条规则 SRC。map 层入口 vm_map_purgable_control 做的事很薄:校验 control 码与掩码、以读锁查 entry(须为非 submap、可写、对象存在且 purgable != DENY),然后加对象锁调用对象状态机;唯一独立功能是 VM_PURGABLE_PURGE_ALL——不理会任何地址、直接全系统回收所有 volatile 对象 SRC vm_map.c:18565-18568。用户态入口 mach_vm_purgable_control 额外拒绝 VM_PURGABLE_SET_STATE_FROM_KERNEL(仅内核可用,供 IOKit 的 kernel-only purgeable 映射)SRC vm_user.c:2124-2127。
18565if (control == VM_PURGABLE_PURGE_ALL) { 18566 vm_purgeable_object_purge_all(); 18567 return KERN_SUCCESS; 18568}
purgeable 的全部状态定义在公共头 osfmk/mach/vm_purgable.h。对象创建时状态初始化为 NONVOLATILE(vm_map.c:2839,且 purgeable 对象永不与相邻 entry 合并,以保证"每个 volatile 对象是独立实体"的语义 SRC vm_map.c:2821-2827):
143/* 144 * Valid states of a purgeable object. 145 */ 146#define VM_PURGABLE_STATE_MIN 0 /* minimum purgeable object state value */ 147#define VM_PURGABLE_STATE_MAX 3 /* maximum purgeable object state value */ 148#define VM_PURGABLE_STATE_MASK 3 /* mask to separate state from group */ 149 150#define VM_PURGABLE_NONVOLATILE 0 /* purgeable object is non-volatile */ 151#define VM_PURGABLE_VOLATILE 1 /* purgeable object is volatile */ 152#define VM_PURGABLE_EMPTY 2 /* purgeable object is volatile and empty */ 153#define VM_PURGABLE_DENY 3 /* (mark) object not purgeable */
状态本体存放在 vm_object 的 2-bit 位域里(连同两个行为位)SRC vm_object.h:265-271:
265 /* boolean_t */ purgable:2, /* Purgable state. See 266 * VM_PURGABLE_* 267 */ 268 /* boolean_t */ purgeable_only_by_kernel:1, 269 /* boolean_t */ purgeable_when_ripe:1, /* Purgeable when a token 270 * becomes ripe. 271 */
mach_vm_purgable_control 的 in/out 参数 state 是一个位域打包值——低 2 位是目标状态,其余位携带排队策略修饰符。这解释了为什么 SET_STATE 的返回值(旧状态)也可能是带修饰位的原值:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +----+-----+----+----+--+---+---+---+---+--------+ | NA |DEBUG | … |GRP | | B |ORD| | STA | +----+-----+----+----+--+---+---+---+---+--------+ ↑ ↑ ↑ ↑ ↑ ↑ 16=NO_AGING 8-10= 6=BEHAVIOR 0-1=状态 (免老化) VOLATILE_GROUP(0-7) FIFO/LIFO (0-3)
vm_purgable.h:63-83 原文位布局:DEBUG(12-13)=DEBUG_EMPTY(0x1000)/DEBUG_FAULT(0x2000);ORDERING_OBSOLETE(0x20) 进 obsolete 队列;BEHAVIOR_LIFO(0x40)| 修饰位 | 值 | 语义 | 源码 |
|---|---|---|---|
VM_PURGABLE_ORDERING_OBSOLETE | 0x20 | 进 obsolete 队列:token 恒为 0,永远成熟,任何回收轮次首选 | vm_purgable.h:129-132;vm_purgeable.c:264-269 |
VM_PURGABLE_BEHAVIOR_FIFO / LIFO | 0x00 / 0x40 | 同组内先入先死 / 后入先死(前向缓存用 LIFO、后向缓存用 FIFO,头文件注释 L26-36) | vm_purgable.h:112-121 |
VM_VOLATILE_GROUP_0..7 | 0x000-0x700 | 8 个回收组(全局),组 0 先死;默认组 0 | vm_purgable.h:99-110 |
VM_PURGABLE_NO_AGING | 0x10000 | 免老化:purgeable_when_ripe=FALSE,不挂 token,与 obsolete 同样随时可回收 | vm_object.c:6013-6018 |
VM_PURGABLE_DEBUG_EMPTY | 0x1000 | 调试:此后 VOLATILE 请求直接落地为 EMPTY | vm_object.c:5893-5895, 5906-5909 |
VM_PURGABLE_DEBUG_FAULT | 0x2000 | 调试:volatile/EMPTY 期间访问 → VM_FAULT_MEMORY_ERROR(SIGBUS),用于抓"读 volatile 内存"的 bug | vm_object.c:5896-5898;vm_fault.c:722-738 |
vm_object_purgable_control() ¶对象层状态机(vm_object.c:5846-6170)是全部问题的答案所在。它自带一段 70 行的 API 语义注释(L5770-5841),其中两句直接回答了本文的两个核心问题——值得整段抄录:
5781 * A purgeable object may be put into a "volatile" state which will make the 5782 * object's pages elligable for being reclaimed without paging to backing 5783 * store if the system runs low on memory. If the pages in a volatile 5784 * purgeable object are reclaimed, the purgeable object is said to have been 5785 * "emptied." When a purgeable object is emptied the system will reclaim as 5786 * many pages from the object as it can in a convenient manner (pages already 5787 * en route to backing store or busy for other reasons are left as is). When 5788 * a purgeable object is made volatile, its pages will generally be reclaimed 5789 * before other pages in the application's working set. This semantic is 5790 * generally used by applications which can recreate the data in the object 5791 * faster than it can be paged in. ... 5794 * A purgeable object may be designated as "non-volatile" which means it will 5795 * behave like all other objects in the system with pages being written to and 5796 * read from backing store as needed to satisfy system memory needs. If the 5797 * object was emptied before the object was made non-volatile, that fact will 5798 * be returned as the old state of the purgeable object (see 5799 * VM_PURGABLE_SET_STATE below). In this case, any pages of the object which 5800 * were reclaimed as part of emptying the object will be refaulted in as 5801 * zero-fill on demand. It is up to the application to note that an object 5802 * was emptied and recreate the objects contents if necessary. When a 5803 * purgeable object is made non-volatile, its pages will generally not be paged 5804 * out to backing store in the immediate future. A purgeable object may also 5805 * be manually emptied.
5833 * The process of making a purgeable object non-volatile and determining its 5834 * previous state is atomic. Thus, if a purgeable object is made 5835 * VM_PURGABLE_NONVOLATILE and the old state is returned as 5836 * VM_PURGABLE_VOLATILE, then the purgeable object's previous contents are 5837 * completely intact and will remain so until the object is made volatile 5838 * again. If the old state is returned as VM_PURGABLE_EMPTY then the object 5839 * was reclaimed while it was in a volatile state and its previous contents 5840 * have been lost.
5863 /* 5864 * Get current state of the purgeable object. 5865 */ 5866 old_state = object->purgable; 5867 if (old_state == VM_PURGABLE_DENY) { 5868 return KERN_INVALID_ARGUMENT; 5869 } ... 5878 if (control == VM_PURGABLE_GET_STATE) { 5879 *state = old_state; 5880 return KERN_SUCCESS; 5881 } ... 5900 new_state = *state & VM_PURGABLE_STATE_MASK; 5901 if (new_state == VM_PURGABLE_VOLATILE) { 5902 if (old_state == VM_PURGABLE_EMPTY) { 5903 /* what's been emptied must stay empty */ 5904 new_state = VM_PURGABLE_EMPTY; 5905 } 5906 if (object->volatile_empty) { 5907 /* debugging mode: go straight to empty */ 5908 new_state = VM_PURGABLE_EMPTY; 5909 } 5910 }
四个要点:
GET_STATE 直接返回 object->purgable(0/1/2),无任何副作用。SET_STATE 的返回值是旧状态(函数末尾 *state = old_state,L6165)。这是应用获知"volatile 期间内容是否被丢"的唯一通道。KERN_INVALID_ARGUMENT。| # | from → to | 触发者 | 内核动作(源码) | SET_STATE 返回 | footprint |
|---|---|---|---|---|---|
| ① | NV → VOL | 应用 | 选队列(obsolete/FIFO/LIFO × 8 组)→ 挂 token(count=当时 inactive 页数,除非 NO_AGING)→ vm_purgeable_object_add 入 objq[group] → vm_page_purgeable_count += 驻留页 → 记账转移(L5975-6104) | 0(旧态 NV) | ↓ 移出(L1581-1588) |
| ② | VOL → NV | 应用 | vm_purgeable_object_remove 出队 → token_delete_last → 记账转移(L5919-5973) | 1=VOL ⇒ 内容完好 | ↑ 计回(L1564-1571) |
| ③ | NV → EMP | 应用 | 先记账转移(footprint↓)→ 置 EMPTY → 同步 vm_object_purge()(L6107-6162) | 0(旧态 NV) | ↓ 移出 |
| ④ | VOL → EMP | 内核 | purge_one/PURGE_ALL/freezer/jetsam 清场 → vm_object_purge():先置 EMPTY 再收割驻留页+压缩器页(L5673-5691) | —(无返回值;应用稍后经 ⑤ 或 GET_STATE 得知) | 不变(本来就不在) |
| ⑤ | EMP → NV | 应用 | 仅记账转移(对象已在 nonvolatile 队列上,无出队操作;L5964-5971) | 2=EMPTY ⇒ 内容已丢,零填充回归 | ↑ 计回(按现存页数) |
| ⑥ | EMP → VOL | — | 不存在:请求被改写回 EMPTY(L5902-5905) | 2(旧态 EMP) | — |
VOL → VOL(重新标 volatile 改组/改队列)合法且常见——用于调整回收优先级;内核刻意不重挂 token("moving priorities will not make pages stay around longer",L6057-6064),避免反复调整的对象被变相续命。5919 case VM_PURGABLE_NONVOLATILE: 5920 object->purgable = new_state; 5921 5922 if (old_state == VM_PURGABLE_VOLATILE) { ... 5943 vm_page_lock_queues(); 5945 /* object should be on a queue */ 5946 assert(object->objq.next != NULL && 5947 object->objq.prev != NULL); ... 5950 /* 5951 * Move object from its volatile queue to the 5952 * non-volatile queue... 5953 */ 5954 queue = vm_purgeable_object_remove(object); 5955 assert(queue); 5956 5957 if (object->purgeable_when_ripe) { 5958 vm_purgeable_token_delete_last(queue); 5959 } ... 5963 } 5964 if (old_state == VM_PURGABLE_VOLATILE || 5965 old_state == VM_PURGABLE_EMPTY) { 5966 /* 5967 * Transfer the object's pages from the volatile to 5968 * non-volatile ledgers. 5969 */ 5970 vm_purgeable_accounting(object, VM_PURGABLE_VOLATILE); 5971 }
6126 if (old_state == VM_PURGABLE_VOLATILE) { ... 6133 old_queue = vm_purgeable_object_remove(object); 6134 assert(old_queue); 6135 if (object->purgeable_when_ripe) { 6136 vm_page_lock_queues(); 6137 vm_purgeable_token_delete_first(old_queue); 6138 vm_page_unlock_queues(); 6139 } 6140 } 6141 6142 if (old_state == VM_PURGABLE_NONVOLATILE) { ... 6148 vm_purgeable_accounting(object, 6149 VM_PURGABLE_NONVOLATILE); ... 6156 object->purgable = VM_PURGABLE_EMPTY; 6157 } 6158 6159 (void) vm_object_purge(object, 0); 6160 assert(object->purgable == VM_PURGABLE_EMPTY); 6161 6162 break; 6163 } 6164 6165 *state = old_state; /* SET_STATE 返回【旧状态】 */
objq[group] 摘下并挂到 purgeable_nonvolatile_queue(vm_purgeable.c:799-806)。所以 EMPTY 对象物理上待在"nonvolatile 队列"里——DEV 内核的统计代码明确处理这种情况:if (state == VM_PURGABLE_EMPTY) { /* 仍按 volatile 记账 */ }(vm_purgeable.c:1256-1260)。"nonvolatile_queue"的真实语义是"不在任何 volatile 组队列上",而非"状态是 NONVOLATILE"——这是读源码时最容易误解的一处。vm_object_purge() ¶所有路径的终点都是 vm_object_purge(object, flags)(vm_object.c:5642-5767)。它做三件事:先改状态、再收割驻留页、最后收割压缩器页:
5658 /* 5659 * We need to set the object's state to VM_PURGABLE_EMPTY *before* 5660 * reaping its pages. We update vm_page_purgeable_count in bulk 5661 * and we don't want vm_page_remove() to update it again for each 5662 * page we reap later. 5663 * 5664 * For the purgeable ledgers, pages from VOLATILE and EMPTY objects 5665 * are all accounted for in the "volatile" ledgers, so this does not 5666 * make any difference. 5667 * If we transitioned directly from NONVOLATILE to EMPTY, 5668 * vm_page_purgeable_count must have been updated when the object 5669 * was dequeued from its volatile queue ... 5671 */ 5672 if (object->purgable == VM_PURGABLE_VOLATILE) { 5673 unsigned int delta; ... 5682 OSAddAtomic(-delta, 5683 (SInt32 *)&vm_page_purgeable_count); ... 5691 object->purgable = VM_PURGABLE_EMPTY; 5692 } 5693 assert(object->purgable == VM_PURGABLE_EMPTY); 5694 5695 object_page_count = object->resident_page_count; 5696 5697 vm_object_reap_pages(object, REAP_PURGEABLE); /* 收割驻留页 */
purgable = EMPTY 是为了让随后逐页调用的 vm_page_remove() 不再重复调整全局计数(批量扣过一次了)——也顺带保证了并发的缺页线程看到的一致状态。vm_compressor_pager_reap_pages() 一并丢弃;若对象正有换页活动进行中则跳过压缩器收割(避免死锁,L5733-5752),这些页留待下次。vm_page_insert 里有专门的补救路径:2003 } else if (owner && 2004 (object->purgable == VM_PURGABLE_VOLATILE || 2005 object->purgable == VM_PURGABLE_EMPTY)) { 2006 assert(!VM_PAGE_WIRED(mem)); 2007 /* more volatile bytes */ 2008 ledger_credit(owner->ledger, 2009 ledger_idx_volatile, 2010 PAGE_SIZE); 2011 } 2012 ... 2019 } else if (object->purgable == VM_PURGABLE_EMPTY && 2020 mem->vmp_q_state == VM_PAGE_ON_THROTTLED_Q) { 2021 /* 2022 * This page belongs to a purged VM object but hasn't 2023 * been purged (because it was "busy"). 2024 * It's in the "throttled" queue and hence not 2025 * visible to vm_pageout_scan(). Move it to a pageable 2026 * queue, so that it can eventually be reclaimed, instead 2027 * of lingering in the "empty" object. 2028 */ 2029 ... 2032 vm_page_deactivate(mem);
vm_pageout_scan() 是常驻换页线程的主循环。每轮先检查是否已达标(free ≥ target 就准备休眠,L2959-2994),未达标则第一件事就是尝试 purge——排在 speculative 老化、vnode 回收、inactive 扫描、压缩/换出之前:
2999 /* 3000 * Before anything, we check if we have any ripe volatile 3001 * objects around. If so, try to purge the first object. 3002 * If the purge fails, fall through to reclaim a page instead. 3003 * If the purge succeeds, go back to the top and reevalute 3004 * the new memory situation. 3005 */ 3006 retval = vps_purge_object(); 3007 3008 if (retval == VM_PAGEOUT_SCAN_NEXT_ITERATION) { 3012 if (object != NULL) { 3013 vm_object_unlock(object); 3014 object = NULL; 3015 } 3017 lock_yield_check = FALSE; 3018 continue; /* purge 成功 → 回到循环顶部重新评估,可能就此收手 */ 3019 }
设计上"一次只 purge 一个对象、purge 完立即重新评估水位"——避免为满足 10MB 缺口而丢掉 500MB 数据的过度回收(vm_purgeable.c:487-493 注释)。
vps_purge_object() 内部还有第二条触发逻辑:当 memorystatus 报告的压力等级超过 Normal 时,无视 token 成熟度、直接对"低于某组号"的对象强制回收:
1847 pressure_level = memorystatus_vm_pressure_level; 1848 1849 if (pressure_level > kVMPressureNormal) { 1850 if (pressure_level >= kVMPressureCritical) { 1851 force_purge = vm_pageout_state.memorystatus_purge_on_critical; 1852 } else if (pressure_level >= kVMPressureUrgent) { 1853 force_purge = vm_pageout_state.memorystatus_purge_on_urgent; 1854 } else if (pressure_level >= kVMPressureWarning) { 1855 force_purge = vm_pageout_state.memorystatus_purge_on_warning; 1856 } 1857 } ... 1860 if (available_for_purge || force_purge) { 1864 if (vm_purgeable_object_purge_one(force_purge, C_DONT_BLOCK)) {
4822 vm_pageout_state.memorystatus_purge_on_warning = 2; 4823 vm_pageout_state.memorystatus_purge_on_urgent = 5; 4824 vm_pageout_state.memorystatus_purge_on_critical = 8;
含义:Warning 压力下组号 < 2 的 volatile 对象(含默认组 0 的全部对象——IOSurface 默认就是组 0)可以被强制回收;Urgent 提到组 5;Critical 提到组 8(= 全部 8 个组,仍不含 obsolete 之外更高保护——但 obsolete 本来就最优先)。应用可以用高组号 + NO_AGING 之外的保护策略调整自己被强制回收的门槛。
这是 purgeable 最精巧的部分。问题:如何定义"一块 volatile 内存放了足够久、可以丢了"?用真实时间不行(前台活跃 app 的缓存不该被丢),用访问频率也不行。XNU 的答案:用系统 inactive 队列的周转量当时钟——每当全系统有任何一页离开 inactive 队列(被回收或被重新激活),token 时钟走一格。
59struct token { 60 token_cnt_t count; 61 token_idx_t prev; 62 token_idx_t next; 63}; ... 71int32_t token_new_pagecount = 0; /* count of pages that will 72 * be added onto token queue */ 73 74int available_for_purge = 0; /* increase when ripe token 75 * added, decrease when ripe 76 * token removed. 77 * protected by page_queue_lock 78 */
487 /* 488 * Decrement token counters. A token counter can be zero, this means the 489 * object is ripe to be purged. It is not purged immediately, because that 490 * could cause several objects to be purged even if purging one would satisfy 491 * the memory needs. Instead, the pageout thread purges one after the other 492 * by calling vm_purgeable_object_purge_one and then rechecking the memory 493 * balance. 494 * 495 * No need to advance obsolete queue - all items are ripe there, 496 * always 497 */ 498 for (i = PURGEABLE_Q_TYPE_FIFO; i < PURGEABLE_Q_TYPE_MAX; i++) { 499 purgeable_q_t queue = &purgeable_queues[i]; 500 uint32_t num_pages = 1; 501 502 /* Iterate over tokens as long as there are unripe tokens. */ 503 while (queue->token_q_unripe) { 504 if (tokens[queue->token_q_unripe].count && num_pages) { 505 tokens[queue->token_q_unripe].count -= 1; 506 num_pages -= 1; 507 } 508 509 if (tokens[queue->token_q_unripe].count == 0) { 510 queue->token_q_unripe = tokens[queue->token_q_unripe].next; 511 available_for_purge++; 512 ... 521 }
vm_purgeable_internal.h:42-65);FIFO 与 LIFO 之间还有 token 迁移机制(choose_and_delete_ripe,L589-673)保证只有一边有成熟 token 时也能公平选受害者受害者挑选(vm_purgeable_object_find_and_lock,L677-812)在 iOS 上额外看 jetsam 优先级——同组内优先丢"属于低优先级进程"的对象:
730 object_task_importance = 0; 731 732 /* 733 * We don't want to use VM_OBJECT_OWNER() here: we want to 734 * distinguish kernel-owned and disowned objects. 735 * Disowned objects have no owner and will have no importance... 736 */ 737 owner = object->vo_owner; 738 if (owner != NULL && owner != VM_OBJECT_OWNER_DISOWNED) { 739#if !XNU_TARGET_OS_OSX 740#if CONFIG_JETSAM 741 object_task_importance = proc_get_memstat_priority((struct proc *)get_bsdtask_info(owner), TRUE); 742#endif /* CONFIG_JETSAM */ 743#else /* !XNU_TARGET_OS_OSX */ 744 object_task_importance = task_importance_estimate(owner); 745#endif /* !XNU_TARGET_OS_OSX */ 746 } 747 748 if (object_task_importance < best_object_task_importance) { 749 if (vm_object_lock_try(object)) { 750 try_lock_succeeded++; ... 755 best_object = object; 756 best_object_task_importance = object_task_importance; 757 best_object_skipped = num_objects_skipped; 758 if (best_object_task_importance == 0) { 759 /* can't get any better: stop looking */ 760 break;
vm_pageout_scan 扫 inactive 队列逐页处理时对 purgeable 对象有特判(L3300-3349):
3300 if (object->copy == VM_OBJECT_NULL) { 3301 /* 3302 * No one else can have any interest in this page. 3303 * If this is an empty purgable object, the page can be 3304 * reclaimed even if dirty. 3305 * If the page belongs to a volatile purgable object, we 3306 * reactivate it if the compressor isn't active. 3307 */ 3308 if (object->purgable == VM_PURGABLE_EMPTY) { ... 3316 if (m->vmp_dirty || m->vmp_precious) { 3317 /* we saved the cost of cleaning this page ! */ 3318 vm_page_purged_count++; 3319 } 3320 goto reclaim_page; 3321 } 3322 3323 if (VM_CONFIG_COMPRESSOR_IS_ACTIVE) { 3324 /* 3325 * With the VM compressor, the cost of 3326 * reclaiming a page is much lower (no I/O), 3327 * so if we find a "volatile" page, it's better 3328 * to let it get compressed rather than letting 3329 * it occupy a full page until it gets purged. 3330 * So no need to check for "volatile" here. 3331 */ 3332 } else if (object->purgable == VM_PURGABLE_VOLATILE) { 3333 /* 3334 * Avoid cleaning a "volatile" page which might 3335 * be purged soon. 3336 */ ... 3348 goto reactivate_page; 3349 }
vm_object_purge 一并收割(§4)。macOS 无压缩器路径才走"重新激活避免清洗"(L3332-3349)。jetsam 选定受害者后、真正下杀手之前,会先尝试回收该进程自己的全部 volatile 对象(无视成熟度),再重新评估是否还需要杀:
5506 if (cause != kMemorystatusKilledVnodes && cause != kMemorystatusKilledZoneMapExhaustion) { 5507 /* 5508 * Genuine memory pressure and not other (vnode/zone) resource exhaustion. 5509 */ ... 5515 networking_memstatus_callout(p, cause); 5516 num_pages_purged = vm_purgeable_purge_task_owned(p->task); 5517 num_pages_reclaimed += num_pages_purged; ... 5537 if (num_pages_reclaimed) { 5542 if (cause == kMemorystatusKilledHiwat) { 5543 uint64_t footprint_in_bytes = get_task_phys_footprint(p->task); 5544 uint64_t memlimit_in_bytes = (((uint64_t)p->p_memstat_memlimit) * 1024ULL * 1024ULL); 5545 success = (footprint_in_bytes <= memlimit_in_bytes); 5546 } else { 5547 success = (memorystatus_avail_pages_below_pressure() == FALSE); ... 5567 if (success) { 5568 memorystatus_purge_before_jetsam_success++; ... 5573 *killed = FALSE; /* 进程免死 */ 5575 return TRUE;
后台进程被冻结(freeze,iOS 的休眠后台机制)时,volatile purgeable 对象不会被冻结——评估阶段直接跳过,实冻阶段显式 purge(何必浪费压缩器空间去保存"可丢弃"的数据):
20568 if (VM_CONFIG_FREEZER_SWAP_IS_ACTIVE) { 20569 /* 20570 * We skip purgeable objects during evaluation phase only. 20571 * If we decide to freeze this process, we'll explicitly 20572 * purge these objects before we go around again with 20573 * 'evaluation_phase' set to FALSE. 20574 */ 20575 20576 if ((src_object->purgable == VM_PURGABLE_EMPTY) || (src_object->purgable == VM_PURGABLE_VOLATILE)) { ... 20583 if (evaluation_phase == FALSE && 20584 (src_object->purgable == VM_PURGABLE_VOLATILE) && 20585 (src_object->ref_count == 1)) { 20586 vm_object_lock(src_object); 20587 vm_object_purge(src_object, 0); 20588 vm_object_unlock(src_object); 20589 } 20590 continue; 20591 }
986 * phys_footprint 987 * Physical footprint: This is the sum of: 988 * + (internal - alternate_accounting) 989 * + (internal_compressed - alternate_accounting_compressed) 990 * + iokit_mapped 991 * + purgeable_nonvolatile ← NONVOLATILE 计入 992 * + purgeable_nonvolatile_compressed ← 压缩的 NONVOLATILE 也计入 993 * + page_table
公式里没有 volatile 项——purgeable 对象的页不进 internal,而是走独立的 volatile/nonvolatile ledger(vm_map.c:18621 注释 "purgeable has its own accounting")。因此:
1545 if (old_state == VM_PURGABLE_VOLATILE || 1546 old_state == VM_PURGABLE_EMPTY) { 1547 /* less volatile bytes in ledger */ 1548 ledger_debit(owner->ledger, 1549 ledger_idx_volatile, 1550 ptoa_64(resident_page_count - wired_page_count)); ... 1556 /* more non-volatile bytes in ledger */ 1557 ledger_credit(owner->ledger, 1558 ledger_idx_nonvolatile, 1559 ptoa_64(resident_page_count - wired_page_count)); ... 1564 if (do_footprint) { 1565 /* more footprint */ 1566 ledger_credit(owner->ledger, 1567 task_ledgers.phys_footprint, 1568 ptoa_64(resident_page_count 1569 + compressed_page_count 1570 - wired_page_count)); 1571 } 1572 } else if (old_state == VM_PURGABLE_NONVOLATILE) { ... 1581 if (do_footprint) { 1582 /* less footprint */ 1583 ledger_debit(owner->ledger, 1584 task_ledgers.phys_footprint, 1585 ptoa_64(resident_page_count 1586 + compressed_page_count 1587 - wired_page_count)); 1588 }
ledger_debit(phys_footprint, resident+compressed−wired) 立即执行——不等任何真实回收发生。应用因此获得了"降 footprint 缓解 jetsam"的即时回报;内核则获得了"随时可丢"的处置权。EXP §9 步骤 S1→S2:footprint 33809→1041 KiB(−32MiB),逐字节对应。反向转换 ⑤ 按转换时刻的现存页数计回——被 purge 过的对象只有重新缺页进来的零页会回归 footprint。转换之后新产生的页(volatile 期间缺页、压缩器搬运)由页级钩子维护:插入 VOLATILE/EMPTY 对象的页记 volatile ledger(vm_resident.c:2003-2011);压缩页数量变化时 vm_object_owner_compressed_update() 按当前状态分流——NONVOLATILE 动 footprint,VOLATILE/EMPTY 只动 volatile_compressed(vm_purgeable.c:1704-1715)。
task_info(TASK_VM_INFO):返回 phys_footprint 与 ledger_purgeable_nonvolatile / _volatile (+_compressed) 等字段 EXP §9 每步快照;task_info(TASK_VM_INFO_PURGEABLE)(task.c:5239):返回三条队列 × 8 组的 count/size 统计(vm_purgeable_stats,vm_purgeable.c:1172-1198);vm_statistics(64) 的 purgeable_count(vm_statistics.h:103)与 jetsam 快照的 purgeable_pages(kern_memorystatus.c:5156-5159)。被 purge 的对象页表项被断开、驻留页被回收。此后应用访问这块虚拟地址(即使对象还是 EMPTY)会走正常缺页:对象是 internal、无 shadow、无页 → 零填充缺页(vm_fault_zero_page)。API 注释的原话就是 "refaulted in as zero-fill on demand"(L5800)EXP §9 步骤 3:PURGE_ALL 后写过的偏移全部读出 0x00。
调试位 VM_PURGABLE_DEBUG_FAULT 把这个"静默零填充"变成显式错误,用来抓"还在读 volatile 内存"的 bug——注意条件是 VOLATILE 或 EMPTY,两者在此再次被同等对待:
/* vm_object.h:412-414 */ ((object)->volatile_fault && \ ((object)->purgable == VM_PURGABLE_VOLATILE || \ (object)->purgable == VM_PURGABLE_EMPTY)) /* vm_fault.c:722-738 (vm_fault_check) */ if (object->shadow_severed || VM_OBJECT_PURGEABLE_FAULT_ERROR(object)) { /* * Either: * 1. the shadow chain was severed, 727 * 2. the purgeable object is volatile or empty and is marked 728 * to fault on access while volatile. 729 * Just have to return an error at this point 730 */ ... return VM_FAULT_MEMORY_ERROR; /* → SIGBUS */ }
EXP §9 步骤 8:VOLATILE|DEBUG_FAULT 下读 → SIGBUS;SET(NONVOLATILE) 后同一地址可读且数据完好(0x77)——对象从未被 purge,只是被"上锁"。WebKit 源码里的 setVolatileAndPurgeForTesting(EMPTY)与 ImageIO 的行为见 §11。
分层回答:
objq[group](vm_purgeable.c:915-995),NONVOLATILE 对象根本不在里面(它在 purgeable_nonvolatile_queue 上);vm_pageout_scan 的 EMPTY 特判(L3308)与 freezer 的 purge(L20584)也都只针对 VOLATILE/EMPTY。因此处于 NONVOLATILE 状态的对象永远不会被 purge。NV→VOL(footprint↓)→ 内核回收(VOL→EMPTY,内容没了)→ SET_STATE(NONVOLATILE):转换照常成功,对象回到 NONVOLATILE,页以零填充按需回归——内容永久丢失。唯一的告知机制:这次 SET_STATE 的返回值(旧状态)= VM_PURGABLE_EMPTY(2)(L6165)。API 注释 L5833-5840 明确了原子性契约:返回 VOLATILE ⇒ "completely intact";返回 EMPTY ⇒ "have been lost"。GET_STATE 永远只会说 NONVOLATILE;"曾经被 purge"这件事在状态机里没有存档。EXP §9 步骤 4:SET(NONVOLATILE) 返回 EMPTY(2),随后 GET_STATE=NONVOLATILE(0)。正确用法(libmalloc 的 malloc_make_nonpurgeable L3269-3272 即如此):检查返回值,==EMPTY 就重建数据。EMPTY 不是与 VOLATILE 并列的第三态,而是 VOLATILE 的"已完成"子态。证据链:
| 维度 | VOLATILE (1) | EMPTY (2) | 源码 |
|---|---|---|---|
| 官方定义 | volatile | volatile and empty | vm_purgable.h:151-152 |
| 存储 | 同一个 2-bit purgable 字段(0/1/2/3) | vm_object.h:265 | |
| 记账 | 都记 volatile ledger、都不进 phys_footprint | vm_object.c:5664-5666;vm_purgeable.c:1545-1546 | |
| 缺页调试宏 | volatile_fault && (VOLATILE || EMPTY) 同一条件 | vm_object.h:412-414 | |
| 内容 | 还在(可丢而未丢) | 已丢(页被收割,再缺页=零填充) | — |
| 队列位置 | 在 volatile 队列 objq[group] | 已移出(挂在 nonvolatile_queue 上) | vm_purgeable.c:799-806 |
| 转换 | 可被内核 purge → EMPTY;可回 NV | 单向门:→VOL 被拒绝;→NV 合法(返回 EMPTY) | vm_object.c:5902-5905 |
| 观测 | GET_STATE 返回 1 或 2 —— 这就是"是否已被回收"的运行时观测手段 | vm_object.c:5878-5881 | |
所以准确的图景是:"volatile"是一段身份(可丢弃 + 不计 footprint),"已被回收(EMPTY)"是这段身份下的一个终局事件标记。状态会在应用不知情时从 VOLATILE 静默翻转为 EMPTY(内核 purge 不通知任何人);应用下一次 GET_STATE 或 SET_STATE(NONVOLATILE) 的返回值才会发现。此外 EMPTY 的边界情形:对象可能仍持有逃过收割的 busy 页与重新缺页的零页(§4)——"EMPTY"承诺的是"内容不可信",不是"必然零页"。
探针 purgprobe.c(arm64,ldid 四项 entitlements 签名,SSH root 直跑):分配 32MiB VM_FLAGS_PURGABLE,完整走一遍状态机,每步用 task_info(TASK_VM_INFO) 抓记账快照。设备内核 xnu-7195.140.46~1(与源码基准 7195.141.2 同族)。全部八组观测与源码预测一致:
[1] allocate 32MiB VM_FLAGS_PURGABLE (初始态应为 NONVOLATILE, vm_map.c:2839)
after fill 0xA5: contents intact
[S1 NONVOLATILE + touched] phys_footprint=33809 KiB | ledger: nonvolatile=32768KiB volatile=0
GET_STATE -> NONVOLATILE(0)
[2] SET_STATE(VOLATILE) — 预期: 记账即时移出 phys_footprint (vm_purgeable.c:1581-1588)
SET_STATE(VOLATILE) -> returned old_state=NONVOLATILE(0)
GET_STATE -> VOLATILE(1)
[S2 VOLATILE] phys_footprint=1041 KiB | ledger: nonvolatile=0 volatile=32768KiB
read ALL pages while VOLATILE (32MiB resident, footprint unchanged?): contents intact
[S2b VOLATILE after full read] phys_footprint=1041 KiB | volatile=32768KiB
[3] VM_PURGABLE_PURGE_ALL — 内核主动回收路径 (vm_map.c:18565, 无视 token 成熟度)
PURGE_ALL kr=0
GET_STATE -> EMPTY(2)
read first page after PURGE_ALL: CONTENTS LOST/CHANGED (written offsets wrong=4/4)
zero-fill check (expect all-zero): contents intact (wrong=0/4)
[S3 EMPTY after purge (1 page refaulted)] volatile=16KiB ← 仅 1 页(16KiB)重新缺页
[4] EMPTY -> NONVOLATILE — 核心问题: 转换成功且【返回旧状态=EMPTY】(vm_object.c:6165)
SET_STATE(NONVOLATILE) -> returned old_state=EMPTY(2)
GET_STATE -> NONVOLATILE(0)
[S4 NONVOLATILE] phys_footprint=1057 KiB | nonvolatile=16KiB ← 1 页零页计回 footprint(+16KiB)
[5] 单向门: EMPTY -> VOLATILE 请求被改写 (vm_object.c:5902-5905)
SET_STATE(VOLATILE) -> old=NONVOLATILE(0); GET_STATE -> VOLATILE(1)
SET_STATE(EMPTY) -> old=VOLATILE(1); GET_STATE -> EMPTY(2)
SET_STATE(VOLATILE(while EMPTY)) -> old=EMPTY(2); GET_STATE -> EMPTY(2) ← 仍 EMPTY
[6] 对照: 普通(非 purgeable)分配 GET_STATE
GET_STATE -> kr=4 (KERN_INVALID_ARGUMENT) ← 与 vm_object.c:5867 一致
[7] DEBUG_EMPTY (0x1000): VOLATILE 请求直接落地为 EMPTY (vm_object.c:5906-5909)
SET_STATE(VOLATILE|DEBUG_EMPTY) -> old=NONVOLATILE(0); GET_STATE -> EMPTY(2)
read after DEBUG_EMPTY: 全零(内容立即丢弃)
[8] DEBUG_FAULT (0x2000): 访问 volatile 页 -> VM_FAULT_MEMORY_ERROR (vm_fault.c:722-738)
SET_STATE(VOLATILE|DEBUG_FAULT) -> old=NONVOLATILE(0)
read while VOLATILE|DEBUG_FAULT -> SIGBUS (VM_FAULT_MEMORY_ERROR) as expected
SET_STATE(NONVOLATILE(rescue)) -> old=VOLATILE(1)
read after NONVOLATILE -> 0x77 (access OK again) ← 未被 purge,内容完好
| # | 观测 | 印证的源码断言 |
|---|---|---|
| EXP-1 | SET_STATE 一律返回旧状态(0/1/2 都出现过) | vm_object.c:6165 *state = old_state |
| EXP-2 | 标记 VOLATILE 瞬间 footprint 33809→1041 KiB(−32MiB 整),ledger 同步搬家 | vm_purgeable.c:1581-1588(§6.2) |
| EXP-3 | volatile ≠ 已回收:32MiB 全量读回 0xA5 完好,且驻留页不计入 footprint | §3 语义;footprint 公式无 volatile 项(task.c:991-992) |
| EXP-4 | PURGE_ALL 后 GET_STATE=EMPTY(2),写过的偏移全部变零;仅重读的 1 页进 volatile ledger(16KiB,16K 页设备) | vm_map.c:18565-18568;零填充(L5800);vm_resident.c:2003-2011 |
| EXP-5 | EMPTY→NONVOLATILE 成功且返回 2;之后 GET_STATE=0,丢失事实不可再查询 | Q1 全部三层语义(§8) |
| EXP-6 | EMPTY 下请求 VOLATILE:返回 2 且 GET_STATE 仍 EMPTY | 单向门 vm_object.c:5902-5905 |
| EXP-7 | 非 purgeable 内存 GET_STATE → KERN_INVALID_ARGUMENT(4) | vm_object.c:5867(也是探测"是否 purgeable"的技巧) |
| EXP-8 | DEBUG_EMPTY 直达 EMPTY;DEBUG_FAULT 触发 SIGBUS、回 NV 后内容完好 | vm_object.c:5893-5909;vm_fault.c:722-738;vm_object.h:412-414 |
诚实记录:首轮探针曾报 "read while VOLATILE: wrong=1"——排查后确认是探针自身采样瑕疵(检查了一个从未写入的偏移 4095,其值本来就是零),与内容丢失无关;修正采样逻辑后复测全绿。原始两轮输出均已归档 raw-data/purgprobe-run.txt。另:PURGE_ALL 是全系统动作,实验恰一次,无可见副作用。
对 vm_purgeable.c 做三版对比(xnu-4903.270.47 / 7195.141.2 / 11215.1.10):
| 对比 | diff 行数 | 实质变化 |
|---|---|---|
| iOS 12 → 14 | 143 | vm_purgeable_disown() 重构移出(任务退出时的所有权放弃逻辑并入 vm_object_ownership_change 体系);断言增强。状态机、token 时钟、记账公式、受害者选择零变化 |
| iOS 14 → 18 | 165 | 纯机械性:kmem 加 guard 分配(kmem_guard_t)、头文件改名(vm_page.h→vm_page_internal.h 等)。算法逐行等价 |
关键机制在三个版本全部在场:jetsam 优先级受害者选择(12:739 / 14:741 / 18:726 附近)、purge_task_owned 清场、ledger 记账、token 状态机。结论:iOS 的 purgeable 状态管理与回收策略自 iOS 12 起保持稳定,本文全部语义分析对 iOS 12–18 均适用。
| 门 | API | 典型调用方 | 状态语义 |
|---|---|---|---|
| A | IOSurfaceSetPurgeable(IOKit 四态直译 VM 三态) | QuartzCore / SkyLight / Metal / CoreImage / CoreML / IOGPU / WebKit 图形栈 | KeepCurrent→GET_STATE、NonVolatile/Volatile/Empty→对应位;IOSurface 公开枚举只暴露 4 基础态 → 默认 组 0(warning 压力即可被强制回收) |
| B | mach_vm_purgable_control / vm_purgable_control | ImageIO / CoreFoundation / JavaScriptCore / MediaToolbox / CoreData | 本文全部语义;libmalloc 的 malloc_make_purgeable/nonpurgeable(门 C 底层)也是它 |
| C | malloc_default_purgeable_zone + malloc_make_purgeable | CoreGraphics / SpotlightIndex / DataDetectorsCore | malloc_make_nonpurgeable 检查返回旧态 == VM_PURGABLE_EMPTY ⇒ 数据已丢(libmalloc malloc.c:3269-3272)——Q1 答案的官方用户态范式 |
BSD 层还有一套页粒度的"可重用页"机制,与 purgeable 并存但完全独立:
1158 case MADV_FREE_REUSABLE: 1159 new_behavior = VM_BEHAVIOR_REUSABLE; 1160 break; 1161 case MADV_FREE_REUSE: 1162 new_behavior = VM_BEHAVIOR_REUSE; 1163 break;
| 维度 | purgeable(本文) | reusable pages(MADV_FREE_REUSABLE) |
|---|---|---|
| 粒度 | 对象级(整块 vm_object) | 页级(vmp_reusable 位 + 对象 all_reusable/reusable_page_count) |
| 状态机 | 三态 + 单向门 + 返回旧态 | 无对外状态;REUSE 只返回成功/失败 |
| 老化 | token 时钟(inactive 周转) | 无——标记即在 pageout 可回收集合里 |
| 记账 | 移出 phys_footprint | 不移出 footprint(仍在 internal) |
| 谁在用 | Apple 图形/媒体栈全面押注 | 第三方跨平台代码(Chromium discardable_shared_memory POSIX 路径) |
/* 置 NONVOLATILE 后检查返回的旧状态 */ int state = VM_PURGABLE_NONVOLATILE; vm_purgable_control(mach_task_self(), (vm_address_t)ptr, VM_PURGABLE_SET_STATE, &state); if (state == VM_PURGABLE_EMPTY) { /* 数据已丢,必须重建 */ }
WebKit 的 IOSurface::setVolatile(调用 IOSurfaceSetPurgeable 后检查 old == kIOSurfacePurgeableEmpty)与 ImageIO 解码 scratch"用完即 EMPTY"(主动同步丢弃换页)都是同一契约的变体——检查 SET_STATE 的返回值是使用 purgeable 内存的强制性纪律。
src/)| 文件 | 关键锚点 | 内容 |
|---|---|---|
osfmk/mach/vm_purgable.h | L58-62 / L63-83 / L143-153 | 控制码 / state 位布局 / 三态常量 |
osfmk/vm/vm_purgeable_internal.h | L42-74 / L130 | 队列与 token 数据结构 / LOOP_MAX=64 |
osfmk/vm/vm_object.h | L265-271 / L412-414 | purgable:2 状态位域 / FAULT_ERROR 宏 |
osfmk/vm/vm_object.c | L5642-5767 / L5770-5841 / L5846-6170 | vm_object_purge / API 语义注释 / 状态机主体 |
osfmk/vm/vm_purgeable.c | L59-78 / L141-146 / L154-309 / L468-542 / L677-812 / L894-1030 / L1256-1260 / L1504-1605 | token 结构 / 不变量 / token_add / 时钟 / 受害者选择 / purge_one / EMPTY 记账 / ledger 转移 |
osfmk/vm/vm_map.c | L2821-2860 / L18538-18640 / L20568-20591 | 对象创建与初始态 / map 层控制 + PURGE_ALL / freezer purge |
osfmk/vm/vm_pageout.c | L1836-1876 / L2999-3019 / L3300-3349 / L4822-4824 | vps_purge_object / "Before anything" / EMPTY vs VOLATILE 页处理 / 组号阈值 |
osfmk/vm/vm_resident.c | L2003-2034 / L8628+8639 / L8788 | EMPTY 幸存页处置 / 时钟 tick / token_new_pagecount++ |
osfmk/vm/vm_fault.c | L717-739 | volatile+DEBUG_FAULT → MEMORY_ERROR |
osfmk/vm/vm_user.c | L2113-2133 | 用户态 trap 入口 |
osfmk/kern/task.c | L986-993 | phys_footprint 公式 |
bsd/kern/kern_memorystatus.c | L5506-5576 | jetsam purge-before-kill 免死逻辑 |
bsd/kern/kern_mman.c | L1124-1176 | madvise → VM_BEHAVIOR 桥接 |
src/ 下按 tag 精确存档的源码文件(jsDelivr 拉取 apple-oss-distributions/xnu@xnu-7195.141.2,与 GitHub raw 字节数一致),可第三方复核;experiments/purgprobe.c、原始输出 raw-data/purgprobe-run.txt 均归档;vm_purgeable.c 全文 diff;