本文是一组围绕"移动端内存管理"的源码级分析,全部基于在真实越狱 iPhone 上的实测 + 官方开源仓库逐行核对。核心方法:
8 个工作负载覆盖:快路径延迟(pair_32b/4kb/1mb)、驻留(hold_20k)、大块(large_1mb)、realloc 增长、线程争用(threaded_4t)、跨线程对象流(shared_churn_6t)。配置依据对齐移动端真实默认:
| 后端 | 配置 | 依据 |
|---|---|---|
| iOS libmalloc | 直接调 libc malloc() | 设备默认区 = DefaultMallocZone 杂志区(非 nano),fork 子进程不降速已验证,代表 App 真实路径 |
| jemalloc 5.3 | 上游默认 = Android Bionic 基线 | dirty/muzzy_decay_ms=10000(10s)、narenas=ncpu、--with-lg-page=14(16K 页) |
| jemalloc0 | 调 decay=0 + arena.<all>.purge | 探索"立即 decommit"调参 |
| mimalloc 2.1.7 | 上游默认 | purge_delay=10ms;mimalloc 无移动端 Bionic 出货默认 |
| mimalloc0 | 调 purge_delay=0 + mi_collect(true) | 与 jemalloc0 对称的调参实验 |
| scudo standalone | LLVM 16.x 上游默认(Darwin 移植) | Android 11+ 默认分配器;无上游 darwin.cpp,自写 Darwin 平台层(os_unfair_lock 替 futex、arc4random 替 getrandom、mach_task_basic_info 替 /proc);DarwinConfig(PrimaryRegionSizeLog=24,因 iOS 拒绝 180GB 虚拟预留);sc_ 前缀 |
| 后端 | peakRSS | retained(释放后) | 32B 速度 | 速度代价 |
|---|---|---|---|---|
| libmalloc | 168 MB | 17 MB | 88 ns | — |
| jemalloc(默认) | 178 MB | 81 MB | 21 ns | — |
| jemalloc0(decay=0) | 178 MB | 6 MB(最低) | 21 ns | 零损失 |
| mimalloc | 194 MB | 194 MB | 28 ns | — |
| mimalloc0 | 194 MB | 194 MB(不变) | 28 ns | 大块 churn 暴跌 100-1900× |
| scudo | 209 MB | 206 MB | 60 ns | 大块 pair_1mb 8298 ns(3.8× 慢于 libmalloc) |
原假设:跨线程对象流动(A 分配的块 B 释放)会触发同核争用+跨核 cache 弹跳,libmalloc 的 per-CPU 杂志锁局部性优势应显现、缩小与 jemalloc 的 gap。workload:6 线程共享 2048 槽池,每线程 malloc(64)+atomic_exchange(slots[i],new)+free(old)。
| 工作负载 | libmalloc ns/op | jemalloc ns/op | mimalloc ns/op | scudo ns/op | libmalloc 慢倍数 |
|---|---|---|---|---|---|
| pair_32b(单线程) | 87.8 | 20.9 | 28.1 | 60.4 | 4.2× vs jemalloc |
| threaded_4t(4线程争用) | 21.6 | 5.1 | 7.2 | 15.5 | 4.2× vs jemalloc |
| shared_churn_6t(跨线程流) | 105.8 | 16.7 | 28.4 | 30.4 | 6.3× vs jemalloc |
每线程速率:libmalloc 1.58M ops/t(跌 7.2×)、jemalloc 10.0M ops/t(跌 4.8×)——libmalloc 跌得更狠,假设被证伪。
根因(源码级):① libmalloc 每操作付两把杂志锁(malloc 的 SZONE_MAGAZINE_PTR_LOCK+free 的锁,libmalloc/magazine_tiny.c:2409/506)叠加争用的 atomic_exchange;② jemalloc 跨线程 free 免费——je_free 对小对象总进当前线程 tcache(不追踪分配者),无 remote-free 惩罚,快路径仍无锁(jemalloc/include/jemalloc/internal/cache_bin.h:348),只付 atomic 争用。→ per-CPU 锁局部性真实但不足以抵消"两次锁/次"的固定开销;jemalloc per-thread 无锁 tcache 在所有吞吐场景都赢。
scudo(LLVM Sanitizer Combined Allocator successor)是 Android 11+ 的默认分配器,设计目标是安全加固而非极致速度。上游 scudo standalone 只有 linux.cpp/fuchsia.cpp/trusty.cpp 三个平台层,没有 Darwin 端口。本次测试写了完整的 Darwin 平台层(darwin.cpp:用 os_unfair_lock 替代 Linux futex、arc4random_buf 替代 getrandom syscall、mach_task_basic_info 替代 /proc/self/statm),是首个在真实 iOS 设备上跑的 scudo benchmark。
| 维度 | libmalloc | jemalloc | mimalloc | scudo |
|---|---|---|---|---|
| pair_32b 速度 | 88 ns | 21 ns | 28 ns | 60 ns |
| threaded_4t | 21.6 ns | 5.1 ns | 7.2 ns | 15.5 ns |
| shared_churn_6t | 105.8 ns | 16.7 ns | 28.4 ns | 30.4 ns |
| pair_1mb(大块) | 2193 ns | 423 ns | 407 ns | 8298 ns(最慢) |
| hold_20k retained | 17 MB | 81 MB | 194 MB | 206 MB(最高) |
两者都有安全加固,但加固的对象和层次完全不同。
| 安全维度 | libmalloc(iOS) | scudo(Android) | 源码依据 |
|---|---|---|---|
| 校验和保护对象 | free list 指针(链表节点) | 每 chunk header(每个块头) | libmalloc: magazine_inline.h:194 / scudo: chunk.h:122 |
| 校验和强度 | arm64e: PAC 全签名 / 非 arm64e: 4-bit | 16-bit CRC32(A14 用 __crc32cd 硬件指令) | magazine_inline.h:236 / chunk.h:23-46 |
| UAF 检测(quarantine) | ❌ 没有 | ✅ free 进 quarantine 延迟回收 | scudo/combined.h:1128 |
| 每块 State 追踪 | ❌ 没有 | ✅ Available/Allocated/Quarantined 三态 | scudo/chunk.h:62 |
| double-free 检测 | ❌ 没有(free list 重链入) | ✅ State≠Allocated 即报错 | scudo/combined.h:568 |
| MTE(硬件标签) | ❌ | ✅ setRandomTag → UAF 100% 硬件检测 | scudo/memtag.h:30 |
| GWP-ASan(概率保护页采样) | ❌ | ✅ #ifdef GWP_ASAN_HOOKS | scudo/combined.h:29 |
| guard page(大块) | ✅ 大块有 | ✅ secondary 前后都有 | magazine_tiny.c:1317 / scudo/secondary.h:25 |
| PAC(arm64e 指针签名) | ✅ ptrauth_sign 签 free list 指针 | ❌ 用 CRC 替代 | magazine_inline.h:196-200 |
| ASLR | ✅ DISABLE_ASLR | ✅ PrimaryEnableRandomOffset | magazine_malloc.c:1707 |
| 释放后内存毒化 | ❌ | ✅ PatternOrZeroFill 0xAB | scudo/common.h:203 |
| 释放栈追踪 | ❌ | ✅ storeDeallocationStackMaybe(MTE 时) | scudo/combined.h:1149 |
| VM tag | ✅ Darwin 原生 | ❌ Linux 无此机制 | magazine_malloc.c |
libmalloc 的加固哲学:"保护 free list 不被腐败,其余交给平台沙箱"。
① free list 指针校验和——两条路径(magazine_inline.h:190-253):
② 没有 UAF 检测:free → 直接链入 free list → 立即可复用。没有 quarantine、没有 State 追踪。UAF 在复用窗口内被静默吞掉。
③ 没有 double-free 检测:第二次 free 只是把指针再链入(如果 checksum 过了)。没有 State 字段说"这个块已经是 Available 了"。
scudo 的加固哲学:"每个 chunk 从分配到释放到回收都有 header 追踪 + CRC32 校验 + quarantine 延迟"。
① 每 chunk header 有 State + CRC32(chunk.h:62-73):8 字节 header 含 ClassId(8bit)/State(2bit)/Origin(2bit)/Size(20bit)/Offset(16bit)/Checksum(16bit)。每次操作都校验——loadHeader 重算 checksum 对比,不符即 reportHeaderCorruption。16-bit CRC32 碰撞 1/65536。
② quarantine——UAF 检测的核心(combined.h:1128-1141):free 不直接回收,State 从 Allocated→Quarantined。进 QuarantineCache 延迟释放。quarantine 窗口内 UAF 访问 → 下次 drain 回收时 header 被腐败 → checksum 不匹配 → 报错。这是 libmalloc 完全没有的机制。
③ MTE——100% 硬件级 UAF 检测(memtag.h:30,combined.h:1158):在 MTE 硬件上,free 时 setRandomTag 换标签。UAF 用旧 tag 访问 → CPU tag check 硬件 trap(SIGSEGV),100% 检测率。libmalloc 不支持 MTE(A14 未实现 MTE)。
④ GWP-ASan——概率性保护页采样(combined.h:29-33):编译时开 GWP_ASAN_HOOKS,抽样分配放 guard page 区域,overflow/underflow 直接 SIGSEGV 并附栈追踪。libmalloc 没有;WebKit 有独立的 bmalloc 做类似的事。
scudo 确实比 libmalloc 快(60 vs 88 ns),安全机制也更全。但"快+安全"不是 iOS 不换 scudo 的原因——libmalloc 有 4 件 scudo 架构层面做不到的事,其中第 1 件是 iOS 流畅性的命根。
这是 iOS App 后台能保命的核心。libmalloc 把"何时还内存"的决策权下放给内核,scudo 架构里没有这个通道:
| libmalloc 协同机制 | scudo 对应物 | 差距 |
|---|---|---|
| MADV_FREE_REUSABLE(Darwin 专属:"我先用着但压力来时让给你") | MADV_DONTNEED(Linux:"我不要了") | 语义完全不同——REUSABLE 让内核优先回收而非杀进程 |
| 收到 memorystatus 压力事件 → malloc_memory_event_handler → pressure_relief 主动 flush | ❌ 没有被动响应内核压力的机制 | scudo 只定时 decay,不会在 jetsam 发信号时主动还 |
| VM_MAKE_TAG 让 jetsam 按任务定位大消耗者 | ❌ Linux 无 VM tag | jetsam 无法精确定位"哪个 App 哪类分配最耗内存" |
| vm_purgable_control 可清除分配 | ❌ scudo 无此抽象 | WebKit 的 purgeable image 靠这个 |
关键反证:把 libmalloc 换成 scudo → App 后台时 scudo 不响应 jetsam 压力信号 → jetsam 看到占着 RAM 不还 → 直接杀进程。这是 iOS 不能接受的结果——流畅性的第一支柱"进程还在"就塌了。
scudo 的 16-bit CRC 可碰撞;libmalloc arm64e 用 ptrauth_sign 硬件密码学签名。对 free list 篡改这一个威胁,iOS 已有比 scudo 更强的方案。scudo 多出的 quarantine/MTE/GWP-ASan 防的是 UAF/overflow——这些在 iOS 上由代码签名 + 沙箱 + WebKit bmalloc 在平台层消化了。
Apple 既造 SoC(A14=6 核)又出 OS,已知固定核数。per-CPU 杂志的锁 cache line 永远只被同一核碰——不跨核弹跳。scudo 的 per-thread TSD(pthread_key_create)每次访问多一层 pthread_getspecific,线程多时 tcache 膨胀。
libmalloc 是 Bertrand Serlet 1999 年设计(magazine_malloc.c:26),25 年来与 XNU/CoreFoundation/Foundation/UIKit 深度耦合:malloc_zone_t 注册机制、scalable_zone_info_task 跨任务安全读(诊断/Instruments 基础)、malloc_memory_event_handler 注册在 dispatch 源上(事件总线)。scudo 没有这些 Darwin 集成。
libmalloc:杂志按当前 CPU 选,不是按线程。libmalloc/magazine_rack.c:228:
unsigned int rack_get_thread_index(rack_t *rack) {
index = _malloc_cpu_number(); // 当前 CPU 号,不是线程号
}
// magazine_tiny.c:2399 tiny_malloc_should_clear
mag_index = rack_get_thread_index(rack) % rack->num_magazines;
杂志数 = 核数:num_magazines = mag_max_magazines()(magazine_malloc.c:1699,注释"per-CPU magazines that scale way better")。
jemalloc:tcache 按线程绑,经 tsd(thread-specific data)。
| 维度 | per-thread(jemalloc) | per-CPU(libmalloc) |
|---|---|---|
| cache 数量 | 随线程数增长(8 线程=8 个) | 恒定=核数(6 个) |
| 同核争用 | 无(各自私有) | 有——同核多线程共享该核杂志,争同一把锁 |
| 跨核争用 | 无 | 无(不同核=不同杂志,锁不弹来弹去) |
| 迁核后 | cache 跟线程走 | cache 留在旧核,但新核的杂志元数据是硬件 cache 热的 |
Apple 为何选 per-CPU:① 核数已知且小(A14=6),杂志数恒定省内存(线程多时 jemalloc tcache 膨胀);② 杂志锁的 cache line 永远只被同一核拿→不跨核弹跳(锁真正开销在跨核 cache line 迁移,per-CPU 避开了);③ 杂志元数据硬件 cache 局部性钉在核上;④ 同核争用在 iOS cooperative 调度下低频。
libmalloc:快路径仍持 magazine 锁,即使命中单槽 cache:magazine_tiny.c:2409
SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr); // ← 加锁
ptr = tiny_mag_ptr->mag_last_free; // 唯一一个 cache 槽
if (mag_last_free_msize == msize) {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr); // 命中也锁了
return ptr;
}
jemalloc:无锁无原子,走线程私有栈:cache_bin.h:348
void *ret = *bin->stack_head; // 线程私有栈,无 lock/atomic
if (likely(low_bits != bin->low_bits_low_water)) {
bin->stack_head = new_head;
*success = true; return ret;
}
libmalloc 的快 cache 仅 mag_last_free 单槽;jemalloc 是深栈(每 size-class 几百个)。libmalloc 每操作还付 free 的校验和(free_list_checksum_ptr,抗堆攻击)。→ 88ns 里大头是:锁开销(~30-40ns)+校验和(~10-15ns)+链表慢路径(~15-20ns)。
libmalloc:region 进 depot 后才逐页 MADV_FREE_REUSABLE(magazine_tiny.c:839,pinned_to_depot 保证锁外 madvise 安全)。回收是 pressure/recirc 驱动;大块走 MADV_REUSABLE death-row cache。
jemalloc:decay_dirty/decay_muzzy(默认 10s,jemalloc/src/pac.c:78)→ purge:MADV_FREE(→muzzy)/MADV_DONTNEED(decommit)。定时、可调、可确定。
→ 解释实测 retained:libmalloc 17MB(pressure 下 depot madvise 起作用)、jemalloc 默认 81MB(10s decay 没触发)、jemalloc0 6MB(decay=0 立即 decommit)。
| 维度 | iOS libmalloc(杂志区) | jemalloc 5.3 |
|---|---|---|
| 快路径定位 | per-CPU(_malloc_cpu_number()) | per-thread(tsd) |
| 快路径锁 | 持锁(命中也锁) | 无锁(线程私有栈) |
| free list | 双向链表,校验和指针 | per-thread 栈 |
| 内存获取 | mach_vm_allocate+VM tag | mmap |
| 回收 | depot 逐页 MADV_FREE_REUSABLE,pressure 驱动 | decay 定时,可调 |
| 安全加固 | 校验和 + guard pages | 默认无 |
源码文件头注释(magazine_malloc.c:26):Author Bertrand Serlet, August 1999; 2008 多线程增强"in the spirit of Hoard"(Emery Berger ASPLOS 2000 的 per-processor heap)。libmalloc 是 Hoard 在 Apple 硬件上的特化,四个核心思想:
内核 kern_memorystatus(jetsam) 监控压力,越阈值
→ 投递 NOTE_MEMORYSTATUS kqueue 事件(带 NORMAL/WARN/CRITICAL 级别)
→ 用户态 dispatch source DISPATCH_SOURCE_TYPE_MEMORYSTATUS 触发
→ libmalloc: malloc_memory_event_handler(event) [malloc.c:3139]
event & MALLOC_MEMORYSTATUS_MASK_PRESSURE_RELIEF
→ malloc_zone_pressure_relief(0, 0) [malloc.c:3142]
→ 每个 zone->pressure_relief(zone, 0) [malloc.c:3181]
→ 杂志区: szone_pressure_relief [magazine_malloc.c:1425]
① tiny/small/medium_madvise_pressure_relief [magazine_tiny.c:976]
遍历每 per-CPU 杂志每 region,RACK_DISPOSE_DELAY
→ 迁 depot → 对每个 free span MADV_FREE_REUSABLE
② Large death-row: 锁外拷出后逐个 mvm_deallocate_pages(全 munmap)
置 flotsam_enabled=FALSE 使压力期新大块 free 不再回缓存
净效果:jetsam 发压力信号后毫秒级内,libmalloc 把所有 free/cached 页归还内核 → jetsam 的回收轮拿走 RAM 而无需杀进程。
scalable_zone_info_task(task, memory_reader_t reader, ...)(magazine_malloc.c:954):跨任务(用 memory_reader_t)安全读 malloc zone 统计。
bench 证 libmalloc 慢 4-6×,但手里的 iPhone 依然丝滑。因为分配器快路径延迟根本不是流畅性的瓶颈——流畅性是一个系统级指标,由完全不同的因素决定。
libmalloc 一次 32B malloc+free = ~88ns。一个 App 每帧(16.6ms@60Hz)做 10 万次 alloc/free(已是很重的逻辑):100,000 × 88ns = 8.8ms < 16.6ms,不掉帧。jemalloc 同负载省下的 6.7ms 在 16.6ms 帧预算里省不出肉眼可感区别。
Android Bionic 用 jemalloc(后换 Scudo),分配器更快。但 Android 卡顿来源是:后台进程多→RAM 竞争→LMK 频繁杀→切回冷启动;ART GC 停顿;RenderThread 与 App 共进程争用。Android 选择了快的分配器,输在了"内存/进程保命"。这反向印证:分配器快慢不是关键,内存策略协同才是。libmalloc 慢但协同好,iOS 反而更流畅。
| 维度 | 内核行为 | 源码依据 |
|---|---|---|
| WKdm 算法可重入 | ✅ 安全 | scratch 是参数传入;nm 显示只引用只读查表(_hashLookupTable/_table_*bits,全 U 导入),无可写全局 |
| 压缩(encode) | ❌ 单线程 | vm_compressor_swap_trigger_thread(init 启动一次)+ compaction_swapper_running 守卫 → 同时仅一个压缩线程 |
| 解压(decode) | ✅ 6 核并发 | compressor_cpus = hinfo.max_cpus(=6),compressor_scratch_bufs[cpu_number()*decode_scratch_size],assert(my_cpu_no < compressor_cpus)(XNU/vm_compressor.c:4298) |
| 模式 | 压缩 | 解压(单核) | 解压(6核并发) | 倍率 |
|---|---|---|---|---|
| ALL_ZEROS | ~4500 | — | — | sv,无解压 |
| MOSTLY_ZEROS | ~3150 | ~52000 | ~51000 | ×0.97(MZV 平凡,带宽限) |
| TYPICAL | ~1600 | ~1980 | ~8000 | ×4.07 |
| RANDOM | ~14000 | — | — | 不可压,无解压 |
TYPICAL 6 核聚合解压 ~8000 MiB/s ≈ 4× 单核——内核真实聚合解压能力。未达理想 6× 因内存带宽/缓存争用。压缩为单线程,~1600 MiB/s 即设备上限,故 bench 单线程压缩有代表性;解压单线程低估内核(已补 6 核并发测)。
| 旧鸿蒙 | 鸿蒙 NEXT(统一渲染) | iOS | |
|---|---|---|---|
| 栅格化 | App 进程内 RenderThread | RS 进程集中栅格化 | backboardd 进程 |
| 合成 | RS 进程 | RS 进程 | backboardd 进程 |
| App 提交 | 已栅格化图层 | 渲染命令树(display list) | layer 树快照 |
统一渲染把"栅格化"从 App 进程搬到 RS 进程——结构上向 iOS 的 backboardd 靠拢。App UI 线程卡顿不再直接卡栅格化,和 iOS"App 卡了当帧仍能渲染"同一机理。渲染耦合维度:撤销原"iOS 略优",改判持平。
核实 OpenHarmony kernel v6.1 的 mm/ 目录,发现一整套 Huawei 自研、非 mainline 的内存子系统:
| OH 内核文件 | 作用 | iOS 对应物 |
|---|---|---|
| memcg_control.c | per-app memcg 记账 | per-task VM tag + ledger |
| memcg_reclaim.c | per-app 定向回收冷页(cgroup_reclaim(sc)) | jetsam 压力回收(libmalloc MADV_FREE_REUSABLE + flush) |
| zswapd.c | 内核线程(kthread_run)做 RAM 内压缩交换到 zram | vm_compressor(WKdm/LZ4) |
| zswap.c | 压缩池(zpool+crypto compressor,默认 lz4/lzo) | WKdm 汇编压缩器 |
| vmpressure.c | 压力通知到用户态 | memorystatus / NOTE_MEMORYSTATUS |
| oom_kill.c(upstream) | 兜底杀进程 | jetsam 兜底杀 |
关键发现:① drivers/staging/android/lowmemorykiller.c 在该分支不存在——OH 不带传统 in-kernel LMK;② memcg_reclaim.c 头注释 (c) 2020-2022 Huawei,引用 hyperhold_inf.h——OH 有个叫 HyperHold 的内存子系统;③ zswapd 的 ub_zram2ufs_ratio/ub_ufs2zram_ratio 是分层压缩交换:冷页先压到 zram(压缩 RAM,快)→ 不够再换到 UFS(闪存),与 iOS"vm_compressor 压 RAM → 极端压力 swap 到闪存"是同一分层策略。
| 维度 | iOS | 鸿蒙 NEXT | 判定 |
|---|---|---|---|
| 渲染耦合 | 进程外 backboardd | RS 集中栅格化+合成 | ≈ 持平 |
| 内存保命 | jetsam+libmalloc 主动协同、vm_compressor | memcg 定向回收+zswapd 压缩+zram↔UFS 分层 | ≈ 结构持平 |
| GC 停顿 | ARC 无 GC | ArkTS 仍有 GC(据称分代/并发) | iOS 占优 |
| 范式负担 | 保留模式+编译期+ARC | 声明式 diff+ArkCompiler AOT | iOS 略优 |
| 算力冗余 | A 系顶尖+统一 | 麒麟追赶+仍多元 | iOS 占优 |
鸿蒙 = "Linux memcg 的细粒度 cgroup 定向回收 + iOS 式系统级压缩"的混合。iOS 干脆没用 cgroup,走 per-task+全局压缩+优先级带这条更简的路。三者 reclaim→compress→kill 都到位,但分工哲学不同:Linux 重在"组内精细定向",iOS 重在"全局压缩最冷+按 App 重要性杀+分配器主动还",鸿蒙两者都要。
鸿蒙若卡,卡在哪(修正版):① GC 停顿(ArkTS,头号嫌疑,iOS 无);② 声明式 diff+复杂 list(AOT 缓解但结构性在);③ 中低端芯片算力不足放大以上。不再大概率卡在"被杀冷启动"(鸿蒙已有分层回收)。若鸿蒙想追平 iOS 流畅性下限,该补的不是分配器(二阶项),而是继续把 ArkTS GC 往接近无感调。
iOS 没有 memcg(cgroups 子系统),但有完整功能对等物,且架构与 Linux memcg 在关键点上不同。XNU kern_memorystatus.c(5355 行)源码级核实:
| iOS 组件 | 源码 | 对应 Linux memcg |
|---|---|---|
| per-task ledger(资源账本) | get_task_phys_footprint/task_get_phys_footprint_limit(:968) | mem_cgroup 的 RSS+swap 记账 |
| phys_footprint 记账指标 | memorystatus_get_task_phys_footprint_page_counts(task, *internal_pages, *internal_compressed_pages, ...)(:826) | memory.usage_in_bytes |
| per-task 内存限制(active/inactive 双限 + fatal/nonfatal) | p_memstat_memlimit/memlimit_active/memlimit_inactive/P_MEMSTAT_FATAL_MEMLIMIT(:973) | memory.limit_in_bytes + oom_control |
| jetsam 优先级带(kill 顺序) | JETSAM_PRIORITY_IDLE/AGING_BAND1..2/.../MAX(:307,909) | cgroup OOM priority |
| vm_compressor(回收先于杀) | kill cause 含 kMemorystatusKilledVMCompressorThrashing/VMCompressorSpaceShortage(:92-93) | memcg reclaim + zswap |
Linux memcg 记账 RSS+swap+cache,可压缩的匿名页按原大小全额计入。iOS phys_footprint 把页拆成 internal_pages(未压缩)+ internal_compressed_pages(已压缩)分别记——压缩后 footprint 变小,App 看起来更省。iOS 的记账天然激励"压缩回收":压缩后 footprint 降、不触限;Linux memcg 除非开 zswap 记账否则激励不同。
Linux memcg = 任意 cgroup 树,可多进程归一组共享预算、可嵌套、父子传播(灵活但 page_cgroup 每页挂钩开销大)。iOS = per-task(进程),Darwin 每任务天生有独立 vm_map,页天然归一 task,无需挂钩,再加优先级带归类。无"多进程共享预算"原生抽象,粒度粗但简单、零挂钩开销。
Linux memcg:一个 memory.limit_in_bytes,超限触发回收。iOS:per-task 有 memlimit_active(前台)+ memlimit_inactive(后台)两套限(:973),加 FATAL/NONFATAL 标志(fatal=超限杀,nonfatal=只回收不杀)。前台 App 给大限、后台小限、自动切换——移动端专用调优,Linux 要靠 cgroup 切换+多 limit 模拟。
Linux memcg 回收是 per-cgroup 定向——try_to_free_mem_cgroup_pages() 回收这个 cgroup 的 LRU。iOS 回收是 系统级压缩 + per-task 压力事件双轨:内核 vm_compressor 压缩全系统最冷页(不挑哪个 task,挑最冷),同时给被压 task 发 memorystatus 压力事件 → 该 task 的 libmalloc malloc_memory_event_handler 跑 pressure_relief → 主动 MADV_FREE_REUSABLE 自己的可回收页。iOS 把回收责任部分下放给分配器(libmalloc 主动提示),Linux memcg 模型里分配器只被动被回收——这是 iOS 一个真实但较小的精细度优势。
Linux memcg:cgroup 超 fatal 限 → 该 cgroup 内 OOM killer 杀一个。iOS:系统压力到阈值 → jetsam 按全系统优先级带从低到高杀(IDLE → AGING_BAND → ... → 前台最后),max_kill_priority(:909)控制杀到哪一带为止。iOS 杀是全局 App 优先级驱动(前台几乎不可能被杀,后台 IDLE 先死),Linux 是组内 OOM,与 App 重要性无关。
| 维度 | Linux memcg | iOS(memorystatus+ledger+vm_compressor) |
|---|---|---|
| 记账 | RSS+swap+cache(压缩页全计) | phys_footprint(压缩页按压缩后计,分开) |
| 分组 | 任意 cgroup 树 | per-task + 优先级带 |
| 限额 | 单 limit | active/inactive 双限 + fatal/nonfatal |
| 回收 | per-cgroup 定向 LRU | 系统级压缩 + per-task 压力事件下放分配器 |
| 杀 | 组内 OOM | 全局按 App 优先级带 |
本文反复提到"Darwin""XNU""Mach""BSD"——这里统一解释。
Darwin 是 Apple 所有操作系统(macOS / iOS / iPadOS / watchOS / tvOS / visionOS)的开源内核 + 核心用户态。它是盖在 GUI 之下的那一层操作系统底座:
┌─────────────────────────────────────┐
│ UIKit / SpringBoard (iOS 的触控 UI) │ ← iOS 专属层
│ AppKit / Aqua (Mac 的桌面 UI) │ ← macOS 专属层
├─────────────────────────────────────┤
│ Darwin ← 就是这一层 │
│ ├─ XNU 内核 (Mach + BSD + IOKit) │
│ ├─ libSystem (libc / libmalloc…) │
│ └─ launchd / 核心守护进程 │
├─────────────────────────────────────┤
│ 硬件 (A14 / M1 / M2 …) │
└─────────────────────────────────────┘
Darwin 是共同的根。iPhone 和 Mac 跑的是同一个内核家族,只是上层 UI 不同——这就是为什么越狱 iPhone 能跑很多 Mac 的命令行工具(uname、sysctl、mach_vm_allocate 都是同一套)。本文测试设备 Darwin 20.6.0 = iOS 14.8(对应 macOS 11 Big Sur 内核版本号)。
Darwin 的内核叫 XNU("X is Not Unix",递归缩写),是一个混合内核,三部分粘在一起跑在同一个内核地址空间:
| 组成 | 来源 | 职责 |
|---|---|---|
| Mach 3.0 | 卡内基梅隆大学微内核 | 任务/线程/端口/消息(IPC)、虚拟内存、调度原语 |
| BSD 层 | FreeBSD 衍生 | POSIX API(fork/exec/信号)、VFS 文件系统、BSD socket 网络、进程模型 |
| I/O Kit | Apple 自研 | C++ 设备驱动框架(显卡/USB/网络/存储驱动都基于它) |
不是学术意义的微内核——Mach 没有跑在用户态,而是与 BSD 共享内核地址空间,是混合内核。
| 源码文件 | Darwin 层级 | 本文用途 |
|---|---|---|
| bsd/kern/kern_memorystatus.c | BSD 层 | jetsam 优先级带 + per-task phys_footprint |
| osfmk/vm/vm_compressor.c | Mach 层 | WKdm 压缩器(解压 6 核并发) |
| osfmk/vm/vm_map.c | Mach 层 | phys_footprint 定义 |
| libmalloc/magazine_tiny.c | libSystem(用户态) | per-CPU 杂志 + free list 校验和 |
scudo 上游只有三个平台层:linux.cpp(Linux futex、getrandom、/proc/self/statm)、fuchsia.cpp、trusty.cpp。没有 Darwin 版——因为 Android/Fuchsia 不跑在 Darwin 上,Google 不会写。Darwin 的 API 跟 Linux 不同:
| 功能 | Linux(scudo 原用) | Darwin(本项目移植) |
|---|---|---|
| 互斥锁 | futex(SYS_futex) | os_unfair_lock |
| 随机数 | getrandom syscall | arc4random_buf |
| RSS 查询 | /proc/self/statm 文件 | mach_task_basic_info(Mach 调用) |
| CRC32 检测 | getauxval(AT_HWCAP) | return true(armv8+ 必有) |
| 页释放提示 | MADV_DONTNEED | 同上(Darwin 也有,但 libmalloc 用更强的 MADV_FREE_REUSABLE) |
所以"Darwin 移植"= 把这些 Linux 特有调用换成 Darwin 等价物。这不是优化,是让 scudo 能在 XNU 内核上编译运行的基础工作。本项目写了 darwin.cpp + DarwinConfig,是首个在真实 iOS 设备上跑的 scudo benchmark。
Apple 在 github.com/apple-oss-distributions 开源了 Darwin 的内核(XNU)和核心库(libmalloc、Libc、libdispatch 等)。这就是能拉到 xnu-7195.141.2 和 libmalloc-521.120.7 源码逐行核对的原因。但 GUI 层(AppKit/UIKit/Aqua)不开源。Darwin 是开源的;macOS/iOS 是闭源的商业产品 = Darwin + 闭源 GUI 框架。