iOS Purgeable 内存管理策略
XNU 内核源码深度解析:状态机 · 回收时机 · 记账模型

基准源码:xnu-7195.141.2(iOS 14.8) · 跨版本核对 iOS 12 / 14 / 18 · 真机验证 iPhone 12 Pro (iOS 14.8, Taurine)
核心文件 vm_purgeable.c / vm_object.c / vm_pageout.c 引用规范 [SRC]=源码行号可证 · [EXP]=真机可复现 生成日期 2026-10-14

太长不看 — 两个核心问题的直接回答

Q1:nonvolatile 状态是否可能"已经被回收"?

态本身:不可能;但内容:可能。 处于 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"(内容销毁+零填充)有本质区别。

Q2:"已被回收"状态与 VOLATILE 状态是什么关系?

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。

§1概述与设计动机

purgeable memory(可清除内存)是 XNU 提供的"应用自愿声明可丢弃"的匿名内存机制:应用分配一块内存(vm_allocate(VM_FLAGS_PURGABLE)),在不用时把它标为 volatile——内核从此有权直接丢弃这些页(不换页、不压缩、无 I/O);应用要用时再标回 nonvolatile,并检查内核是否已经把内容丢了(丢了就得重建)。它是 iOS 上"可再生的缓存"(图片解码缓冲、IOSurface 像素缓冲、字体缓存等)的标准载体。

内核侧的设计目标可以概括为三层:

注意拼写:内核把公开 API 拼成 vm_purgable_control(少一个 e),其余内部符号拼 purgeable。这是历史遗留,vm_purgeable_internal.h:25-35 的注释专门解释了这条规则 SRC。

调用链总览

用户态mach_vm_purgable_control() 内核 trapvm_user.c:2113 map 层vm_map_purgable_control() 18539 对象状态机vm_object_purgable_control() 5846 token 引擎vm_purgeable.c(队列/时钟/记账) 页收割vm_object_purge() 5643 → ledger/队列操作
图 1-1 · purgeable 控制流:用户态 → trap → map 层(校验 entry / 对象是否 purgeable)→ 对象状态机(三态转换 + token/记账副作用)

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。

osfmk/vm/vm_map.cL18565-18568 · PURGE_ALL 旁路
18565if (control == VM_PURGABLE_PURGE_ALL) {
18566	vm_purgeable_object_purge_all();
18567	return KERN_SUCCESS;
18568}

§2状态与常量定义

2.1 三态 + DENY:一个 2-bit 字段

purgeable 的全部状态定义在公共头 osfmk/mach/vm_purgable.h。对象创建时状态初始化为 NONVOLATILE(vm_map.c:2839,且 purgeable 对象永不与相邻 entry 合并,以保证"每个 volatile 对象是独立实体"的语义 SRC vm_map.c:2821-2827):

osfmk/mach/vm_purgable.hL143-153 · 状态常量(原文注释)
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:

osfmk/vm/vm_object.hL265-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	                                                */

2.2 state 参数的位布局:状态只是低 2 位

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)
图 2-1 · vm_purgable.h:63-83 原文位布局:DEBUG(12-13)=DEBUG_EMPTY(0x1000)/DEBUG_FAULT(0x2000);ORDERING_OBSOLETE(0x20) 进 obsolete 队列;BEHAVIOR_LIFO(0x40)
修饰位值语义源码
VM_PURGABLE_ORDERING_OBSOLETE0x20进 obsolete 队列:token 恒为 0,永远成熟,任何回收轮次首选vm_purgable.h:129-132;vm_purgeable.c:264-269
VM_PURGABLE_BEHAVIOR_FIFO / LIFO0x00 / 0x40同组内先入先死 / 后入先死(前向缓存用 LIFO、后向缓存用 FIFO,头文件注释 L26-36)vm_purgable.h:112-121
VM_VOLATILE_GROUP_0..70x000-0x7008 个回收组(全局),组 0 先死;默认组 0vm_purgable.h:99-110
VM_PURGABLE_NO_AGING0x10000免老化:purgeable_when_ripe=FALSE,不挂 token,与 obsolete 同样随时可回收vm_object.c:6013-6018
VM_PURGABLE_DEBUG_EMPTY0x1000调试:此后 VOLATILE 请求直接落地为 EMPTYvm_object.c:5893-5895, 5906-5909
VM_PURGABLE_DEBUG_FAULT0x2000调试:volatile/EMPTY 期间访问 → VM_FAULT_MEMORY_ERROR(SIGBUS),用于抓"读 volatile 内存"的 bugvm_object.c:5896-5898;vm_fault.c:722-738

§3状态机核心:vm_object_purgable_control()

对象层状态机(vm_object.c:5846-6170)是全部问题的答案所在。它自带一段 70 行的 API 语义注释(L5770-5841),其中两句直接回答了本文的两个核心问题——值得整段抄录:

osfmk/vm/vm_object.cL5781-5805 · API 语义注释(一):volatile / emptied / 零填充
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.
osfmk/vm/vm_object.cL5833-5840 · API 语义注释(二):原子性与返回值契约 ★
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.

3.1 状态机全景图

NONVOLATILE (0) • 记入 phys_footprint • ledger: purgeable_nonvolatile • 不会被 purge(可被压缩,内容保留) • 在 purgeable_nonvolatile_queue 上 VOLATILE (1) • 移出 phys_footprint(瞬间) • ledger: purgeable_volatile • 在 objq[group] 上 + token 计时 • 数据仍在(≠已回收) • 访问合法(缺页/读均可) EMPTY (2) = volatile + 已清空 • 定义:volatile and empty(头文件原文) • 页已被收割 → 再缺页 = 零填充 • ledger 仍是 volatile 家族 • 不在 volatile 队列(已移至 nonvolatile_q) • 可能有幸存页(busy 页逃过收割) • 脏页可被直接回收(免清洗) ① SET_STATE(VOLATILE) 入队+挂 token · footprint↓ · 返回旧态 ② SET_STATE(NONVOLATILE) 出队+删 token · footprint↑ · 返回旧态(1=完好) ③ SET_STATE(EMPTY) 同步 purge · footprint↓ ④ 内核回收(多种触发) purge_one / PURGE_ALL / freezer / jetsam 清场:vm_object_purge() ⑤ SET_STATE(NONVOLATILE) 返回旧态 = EMPTY(2) ⇒ 内容已丢(唯一通知) 页以零填充回归 · 此后丢失事实不可查询 ✗ 单向门 EMPTY→VOLATILE 被改写回 EMPTY (5902-5905)
图 3-1 · purgeable 三态状态机(转换边编号对应 §3.3 表格;蓝=应用显式转换,红=内核动作,绿=恢复路径)

3.2 入口:GET_STATE 直读,SET_STATE 返回旧状态

osfmk/vm/vm_object.cL5863-5910 · 读旧态 / GET / 单向门 ★
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	}

四个要点:

3.3 六条转换边的完整清单

#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),避免反复调整的对象被变相续命。

3.4 关键转换的源码(节选)

vm_object.cL5919-5971 · ② VOL→NV
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		}
vm_object.cL6126-6165 · ③④ 到达 EMPTY + 返回旧态 ★
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 返回【旧状态】 */
容易被忽略的细节(EMPTY 对象不在 volatile 队列上):内核 purge 一个 VOLATILE 对象时,受害者选择函数会把它从 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"——这是读源码时最容易误解的一处。

§4回收执行:vm_object_purge()

所有路径的终点都是 vm_object_purge(object, flags)(vm_object.c:5642-5767)。它做三件事:先改状态、再收割驻留页、最后收割压缩器页:

osfmk/vm/vm_object.cL5658-5697 · 先置 EMPTY 再收割(顺序有讲究)
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);  /* 收割驻留页 */
osfmk/vm/vm_resident.cL2003-2034 · EMPTY 对象的幸存页处置
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);
因此"EMPTY"的准确语义是"内容已不可靠",而不是"必然零页":EMPTY 对象可能仍持有 (a) 逃过收割的 busy 页——旧数据还在但对象已声明丢弃;(b) 重新缺页进来的新零页。两者对记账都记 volatile、对缺页路径都与 VOLATILE 同等对待。应用唯一正确的做法是把 EMPTY 当作"内容全丢"处理(重建),不要试图利用幸存页。

§5回收时机策略:谁、何时、按什么顺序丢

5.1 触发点一:vm_pageout_scan 的第一顺位

vm_pageout_scan() 是常驻换页线程的主循环。每轮先检查是否已达标(free ≥ target 就准备休眠,L2959-2994),未达标则第一件事就是尝试 purge——排在 speculative 老化、vnode 回收、inactive 扫描、压缩/换出之前:

osfmk/vm/vm_pageout.cL2999-3019 · "Before anything"
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 注释)。

5.2 触发点二:内存压力等级旁路(force purge)

vps_purge_object() 内部还有第二条触发逻辑:当 memorystatus 报告的压力等级超过 Normal 时,无视 token 成熟度、直接对"低于某组号"的对象强制回收:

vm_pageout.cL1844-1873 · 压力等级 → force_purge
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)) {
vm_pageout.cL4822-4824 · 组号阈值(默认值)
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 之外的保护策略调整自己被强制回收的门槛。

5.3 成熟度时钟:token 状态机("年龄"以 inactive 周转计)

这是 purgeable 最精巧的部分。问题:如何定义"一块 volatile 内存放了足够久、可以丢了"?用真实时间不行(前台活跃 app 的缓存不该被丢),用访问频率也不行。XNU 的答案:用系统 inactive 队列的周转量当时钟——每当全系统有任何一页离开 inactive 队列(被回收或被重新激活),token 时钟走一格。

inactive 队列(全系统) 页进入:vm_page_enqueue_inactive() token_new_pagecount++ (L8788) 页离开:vm_page_deactivate_internal() vm_purgeable_q_advance_all() (L8628/8639) token 队列(每 purgeable 队列一条) token_add: count ← 当时 inactive 页数 q_advance_all: 每页离队 count−− count==0 ⇒ ripe(成熟) 成熟即可回收 available_for_purge++ pageout 扫描时逐对象回收 回收一个 → 重新评估水位 count = 0 ✓成熟(可回收) count = 0 ✓成熟(可回收) count = 7unripe(未成熟) count = 23unripe(最年轻) token_q_unripe 指针 不变量 [SRC vm_purgeable.c:141-146]:Σtoken.count + new_pages + token_new_pagecount == vm_page_inactive_count − vm_page_cleaned_count ⇒ 时钟刻度就是 inactive 队列的实际周转:系统闲 ⇒ 时钟停 ⇒ volatile 内存可以无限期保留
图 5-1 · token 成熟度时钟:挂起时刻的 inactive 页数被"记入" token,此后每有一页离开 inactive 就消耗一格
osfmk/vm/vm_purgeable.cL59-78 · 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                                                 */
osfmk/vm/vm_purgeable.cL487-521 · 时钟走格与成熟
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		}

5.4 队列结构与受害者选择

OBSOLETE 队列ORDERING_OBSOLETE / NO_AGING 对象 token 恒 count=0("all obsolete items are ripe immediately" L264-269)→ 永远最优先被回收,无老化期 FIFO 队列(BEHAVIOR_FIFO,默认)先标先死 objq[0] ① objq[1] objq[2] … objq[7] ⑧ 每组一条对象链表 + 队列级 token 链 LIFO 队列(BEHAVIOR_LIFO)后标先死 objq[0] ① objq[1] … objq[7] ⑧ 回收顺序:OBSOLETE → FIFO/LIFO 的组 0 → 组 1 → … → 组 7
图 5-2 · 三条 purgeable 队列 × 8 个 volatile 组(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 优先级——同组内优先丢"属于低优先级进程"的对象:

osfmk/vm/vm_purgeable.cL730-760 · 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;

5.5 iOS 特色:volatile 脏页交压缩器,EMPTY 脏页免清洗直接回收

vm_pageout_scan 扫 inactive 队列逐页处理时对 purgeable 对象有特判(L3300-3349):

osfmk/vm/vm_pageout.cL3300-3349 · EMPTY vs VOLATILE 的页处理差异
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			}

5.6 触发点三:jetsam 杀进程前的"清场可免死"

jetsam 选定受害者后、真正下杀手之前,会先尝试回收该进程自己的全部 volatile 对象(无视成熟度),再重新评估是否还需要杀:

bsd/kern/kern_memorystatus.cL5506-5575 · purge-before-kill
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;

5.7 触发点四:freezer 冻结前先丢弃 volatile

后台进程被冻结(freeze,iOS 的休眠后台机制)时,volatile purgeable 对象不会被冻结——评估阶段直接跳过,实冻阶段显式 purge(何必浪费压缩器空间去保存"可丢弃"的数据):

osfmk/vm/vm_map.cL20568-20591 · vm_map_freeze 对 purgeable 的处理
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					}

§6记账模型:phys_footprint 的即时进出

6.1 footprint 公式:purgeable 单列两项

osfmk/kern/task.cL986-993 · phys_footprint 组成(原文)
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")。因此:

6.2 状态转换时的记账动作

osfmk/vm/vm_purgeable.cL1545-1588 · vm_purgeable_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		}
这就是"策略"最核心的一环:标记 volatile 的那一刻,ledger_debit(phys_footprint, resident+compressed−wired) 立即执行——不等任何真实回收发生。应用因此获得了"降 footprint 缓解 jetsam"的即时回报;内核则获得了"随时可丢"的处置权。EXP §9 步骤 S1→S2:footprint 33809→1041 KiB(−32MiB),逐字节对应。反向转换 ⑤ 按转换时刻的现存页数计回——被 purge 过的对象只有重新缺页进来的零页会回归 footprint。

6.3 页粒度的持续记账

转换之后新产生的页(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)。

6.4 观测面

§7再缺页路径:零填充与调试故障

被 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,两者在此再次被同等对待:

osfmk/vm/vm_object.h + vm_fault.cL412-414 + L722-738
/* 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。

§8核心问题解答(综合源码 + 实验)

Q1:nonvolatile 是否可能是"已经被回收"的?

分层回答:

  1. 状态语义层——不可能。"被内核回收"只发生在 VOLATILE 家族:受害者选择只扫三条 volatile 队列的 objq[group](vm_purgeable.c:915-995),NONVOLATILE 对象根本不在里面(它在 purgeable_nonvolatile_queue 上);vm_pageout_scan 的 EMPTY 特判(L3308)与 freezer 的 purge(L20584)也都只针对 VOLATILE/EMPTY。因此处于 NONVOLATILE 状态的对象永远不会被 purge。
  2. 内容语义层——完全可能,且有一条隐蔽通道。路径: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"。
  3. 信息时效层——只有一次机会。错过返回值之后,GET_STATE 永远只会说 NONVOLATILE;"曾经被 purge"这件事在状态机里没有存档。EXP §9 步骤 4:SET(NONVOLATILE) 返回 EMPTY(2),随后 GET_STATE=NONVOLATILE(0)。正确用法(libmalloc 的 malloc_make_nonpurgeable L3269-3272 即如此):检查返回值,==EMPTY 就重建数据。
  4. 概念澄清层——"离开 DRAM"≠"被回收"。NONVOLATILE 页在压力下仍会被压缩器收编(留在 footprint、内容保留、缺页透明解压)——这是普通换页语义,任何匿名页都有;purgeable 特有的"purge"= 内容销毁 + 零填充回归,只属于 VOLATILE/EMPTY。混用这两个概念会得出"nonvolatile 也可能丢数据"的错误结论。

Q2:"已被回收"状态与 VOLATILE 状态是什么关系?

EMPTY 不是与 VOLATILE 并列的第三态,而是 VOLATILE 的"已完成"子态。证据链:

维度VOLATILE (1)EMPTY (2)源码
官方定义volatilevolatile and emptyvm_purgable.h:151-152
存储同一个 2-bit purgable 字段(0/1/2/3)vm_object.h:265
记账都记 volatile ledger、都不进 phys_footprintvm_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"承诺的是"内容不可信",不是"必然零页"。

一句话总结两个问题的关系:EMPTY 是 volatile 的墓碑;NONVOLATILE 是内容的保险箱。"墓碑变保险箱"(⑤)合法但会清空内容并只在返回值里留一句遗言;"保险箱永不入墓"——但塞进去的东西可能早就在墓里待过。

§9真机实验:iPhone 12 Pro / iOS 14.8

探针 purgprobe.c(arm64,ldid 四项 entitlements 签名,SSH root 直跑):分配 32MiB VM_FLAGS_PURGABLE,完整走一遍状态机,每步用 task_info(TASK_VM_INFO) 抓记账快照。设备内核 xnu-7195.140.46~1(与源码基准 7195.141.2 同族)。全部八组观测与源码预测一致:

purgprobe run @ iPhone13,3 · 2026-10-14 · raw-data/purgprobe-run.txt
[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-1SET_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-3volatile ≠ 已回收:32MiB 全量读回 0xA5 完好,且驻留页不计入 footprint§3 语义;footprint 公式无 volatile 项(task.c:991-992)
EXP-4PURGE_ALL 后 GET_STATE=EMPTY(2),写过的偏移全部变零;仅重读的 1 页进 volatile ledger(16KiB,16K 页设备)vm_map.c:18565-18568;零填充(L5800);vm_resident.c:2003-2011
EXP-5EMPTY→NONVOLATILE 成功且返回 2;之后 GET_STATE=0,丢失事实不可再查询Q1 全部三层语义(§8)
EXP-6EMPTY 下请求 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-8DEBUG_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 是全系统动作,实验恰一次,无可见副作用。

§10跨版本稳定性:iOS 12 → 18

对 vm_purgeable.c 做三版对比(xnu-4903.270.47 / 7195.141.2 / 11215.1.10):

对比diff 行数实质变化
iOS 12 → 14143vm_purgeable_disown() 重构移出(任务退出时的所有权放弃逻辑并入 vm_object_ownership_change 体系);断言增强。状态机、token 时钟、记账公式、受害者选择零变化
iOS 14 → 18165纯机械性: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 均适用。

§11用户态调用面与 madvise 对照

11.1 三条入口(全系统普查结论,dyld 共享缓存符号级)

门API典型调用方状态语义
AIOSurfaceSetPurgeable(IOKit 四态直译 VM 三态)QuartzCore / SkyLight / Metal / CoreImage / CoreML / IOGPU / WebKit 图形栈KeepCurrent→GET_STATE、NonVolatile/Volatile/Empty→对应位;IOSurface 公开枚举只暴露 4 基础态 → 默认 组 0(warning 压力即可被强制回收)
Bmach_vm_purgable_control / vm_purgable_controlImageIO / CoreFoundation / JavaScriptCore / MediaToolbox / CoreData本文全部语义;libmalloc 的 malloc_make_purgeable/nonpurgeable(门 C 底层)也是它
Cmalloc_default_purgeable_zone + malloc_make_purgeableCoreGraphics / SpotlightIndex / DataDetectorsCoremalloc_make_nonpurgeable 检查返回旧态 == VM_PURGABLE_EMPTY ⇒ 数据已丢(libmalloc malloc.c:3269-3272)——Q1 答案的官方用户态范式

11.2 对照机制:madvise(MADV_FREE_REUSABLE / MADV_FREE_REUSE)

BSD 层还有一套页粒度的"可重用页"机制,与 purgeable 并存但完全独立:

bsd/kern/kern_mman.cL1158-1163 · madvise 桥接
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 路径)

11.3 框架层规范用法(以 libmalloc / WebKit 为例)

libmalloc src/malloc.cL3269-3272 · malloc_make_nonpurgeable(官方范式)
/* 置 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 内存的强制性纪律。

§12附录

12.1 引用文件与行号索引(xnu-7195.141.2,存档于本 workspace src/)

文件关键锚点内容
osfmk/mach/vm_purgable.hL58-62 / L63-83 / L143-153控制码 / state 位布局 / 三态常量
osfmk/vm/vm_purgeable_internal.hL42-74 / L130队列与 token 数据结构 / LOOP_MAX=64
osfmk/vm/vm_object.hL265-271 / L412-414purgable:2 状态位域 / FAULT_ERROR 宏
osfmk/vm/vm_object.cL5642-5767 / L5770-5841 / L5846-6170vm_object_purge / API 语义注释 / 状态机主体
osfmk/vm/vm_purgeable.cL59-78 / L141-146 / L154-309 / L468-542 / L677-812 / L894-1030 / L1256-1260 / L1504-1605token 结构 / 不变量 / token_add / 时钟 / 受害者选择 / purge_one / EMPTY 记账 / ledger 转移
osfmk/vm/vm_map.cL2821-2860 / L18538-18640 / L20568-20591对象创建与初始态 / map 层控制 + PURGE_ALL / freezer purge
osfmk/vm/vm_pageout.cL1836-1876 / L2999-3019 / L3300-3349 / L4822-4824vps_purge_object / "Before anything" / EMPTY vs VOLATILE 页处理 / 组号阈值
osfmk/vm/vm_resident.cL2003-2034 / L8628+8639 / L8788EMPTY 幸存页处置 / 时钟 tick / token_new_pagecount++
osfmk/vm/vm_fault.cL717-739volatile+DEBUG_FAULT → MEMORY_ERROR
osfmk/vm/vm_user.cL2113-2133用户态 trap 入口
osfmk/kern/task.cL986-993phys_footprint 公式
bsd/kern/kern_memorystatus.cL5506-5576jetsam purge-before-kill 免死逻辑
bsd/kern/kern_mman.cL1124-1176madvise → VM_BEHAVIOR 桥接

12.2 术语表

purgeable / purgable
同一概念的两种拼写(历史遗留,见 vm_purgeable_internal.h:25-35)
volatile
应用声明"可丢弃"的身份:可被内核零成本回收 + 不计 footprint
emptied / purged
volatile 对象的页被内核实际收割的事件;此后对象状态为 EMPTY
ripe
token 计数归零 = 对象"成熟"(aging 完成),进入可回收集合
obsolete
ORDERING_OBSOLETE 修饰:跳过老化,永远最优先回收
phys_footprint
iOS jetsam 使用的物理占用指标;volatile purgeable 页不计入
zero-fill refault
被 purge 后再访问:缺页处理分配零页(内容不可恢复)

12.3 方法与可信度声明