← 返回书架首页

分配器对比与跨平台内存管理分析

源码级实测:libmalloc vs jemalloc vs mimalloc vs scudo · jetsam 协同链 · 为什么 iOS 慢却流畅 · 鸿蒙 NEXT 统一渲染+memcg/zswapd · iOS 的 memcg 对等物
设备:iPhone 12 Pro(A14 / iOS 14.8 / Darwin 20.6.0,越狱)· 16K 页 · 源码:XNU xnu-7195.141.2、libmalloc-521.120.7、jemalloc 5.3.0、mimalloc 2.1.7、OpenHarmony kernel_linux_5.10 v6.1

目录

  1. 概述与方法论
  2. 分配器实测对比(libmalloc / jemalloc / mimalloc / scudo)
    1. libmalloc vs scudo 安全性源码级对比
    2. 为什么 iOS 不用 scudo 替代 libmalloc
  3. 源码级对比:libmalloc vs jemalloc
  4. libmalloc 设计思想与 jetsam 协同链
  5. 为什么 libmalloc 慢但 iOS 流畅
  6. 内核 WKdm 多线程解压
  7. 鸿蒙 NEXT 对比:统一渲染 + memcg/zswapd
  8. iOS 的 memcg 对等物:per-task ledger + phys_footprint
  9. 背景:Darwin 与 XNU 内核是什么
  10. 结论与方法论

0. 概述与方法论

本文是一组围绕"移动端内存管理"的源码级分析,全部基于在真实越狱 iPhone 上的实测 + 官方开源仓库逐行核对。核心方法:

诚实边界:本文对 XNU / libmalloc / jemalloc / mimalloc / OpenHarmony 内核的判断是源码级核对过的;对鸿蒙 ArkUI 运行时、ArkTS GC 的细节属公开架构 + 推理(无 ArkTS 运行时源码逐行核对),均已标出不确定处。鸿蒙用户态杀进程调度器未在仓库列表精确定位(可能在 foundation 大仓内)。

1. 分配器实测对比(libmalloc / jemalloc / mimalloc)

1.1 测试方法与配置依据

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 出货默认
mimalloc0purge_delay=0 + mi_collect(true)与 jemalloc0 对称的调参实验
scudo standaloneLLVM 16.x 上游默认(Darwin 移植)Android 11+ 默认分配器;无上游 darwin.cpp自写 Darwin 平台层os_unfair_lock 替 futex、arc4random 替 getrandom、mach_task_basic_info 替 /proc);DarwinConfigPrimaryRegionSizeLog=24,因 iOS 拒绝 180GB 虚拟预留);sc_ 前缀

1.2 实测结果(hold_20k:live≈170MB)

后端peakRSSretained(释放后)32B 速度速度代价
libmalloc168 MB17 MB88 ns
jemalloc(默认)178 MB81 MB21 ns
jemalloc0(decay=0)178 MB6 MB(最低)21 ns零损失
mimalloc194 MB194 MB28 ns
mimalloc0194 MB194 MB(不变)28 ns大块 churn 暴跌 100-1900×
scudo209 MB206 MB60 ns大块 pair_1mb 8298 ns(3.8× 慢于 libmalloc)
关键发现 1:decay 调参是 jemalloc 的免费午餐。jemalloc0 让 retained 81→6MB(甚至低于 libmalloc 的 17MB),速度零损失(pair_32b 21→21ns,threaded 5.1→5.1ns,realloc/large 不变)。→ 调 decay 后 jemalloc 综合最优。
关键发现 2:mimalloc 调参却是坏调参(对称实验)。mimalloc0 的 purge_delay=0 + mi_collect(true)
  1. retained 仍 194MB(194000→193984,几乎不动)——源码 mimalloc/src/prim/unix/prim.c:397 显示 decommit 走 MADV_DONTNEED,但 mi_collect 只回收 abandoned/retired 段,不释放当前工作集段(保留以复用)。
  2. 大块 churn 暴跌:purge_delay=0 使每次 free 同步触发 purge,大块(1MB) 每次释放即 madvise+重提交 → pair_1mb 410→43359 ns/op(106×)、large_1mb 368→708969 ns/op(1927×)
mimalloc 无"省内存又不损速"的好旋钮,与 jemalloc0 恰反。

1.3 shared_churn_6t:验证假设·证伪

原假设:跨线程对象流动(A 分配的块 B 释放)会触发同核争用+跨核 cache 弹跳,libmalloc 的 per-CPU 杂志锁局部性优势应显现、缩小与 jemalloc 的 gap。workload:6 线程共享 2048 槽池,每线程 malloc(64)+atomic_exchange(slots[i],new)+free(old)

工作负载libmalloc ns/opjemalloc ns/opmimalloc ns/opscudo ns/oplibmalloc 慢倍数
pair_32b(单线程)87.820.928.160.44.2× vs jemalloc
threaded_4t(4线程争用)21.65.17.215.54.2× vs jemalloc
shared_churn_6t(跨线程流)105.816.728.430.46.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 在所有吞吐场景都赢。

1.4 scudo:安全优先的加固分配器

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。

维度libmallocjemallocmimallocscudo
pair_32b 速度88 ns21 ns28 ns60 ns
threaded_4t21.6 ns5.1 ns7.2 ns15.5 ns
shared_churn_6t105.8 ns16.7 ns28.4 ns30.4 ns
pair_1mb(大块)2193 ns423 ns407 ns8298 ns(最慢)
hold_20k retained17 MB81 MB194 MB206 MB(最高)
scudo 的定位:安全中间件。在四个分配器里 scudo 是唯一以安全为首要目标的。它比 libmalloc 快 31%(60 vs 88 ns)但比 jemalloc 慢 2.9×(60 vs 21 ns)。这个"不快不慢"的位置正是安全加固的代价。
  1. per-chunk 校验和:每个块头嵌入 16-bit CRC32(A14 用 __crc32cd 硬件指令,scudo/checksum.cpp:66)——检测堆腐败/UAF,但每 free 多一次校验。libmalloc 也有校验和(free_list_checksum_ptr),但 scudo 还多了 quarantine。
  2. 延迟回收队列(quarantine):free 的块不立即归还,先进 quarantine 缓存延迟释放(scudo/quarantine.h:297),让 UAF 在窗口期内触发校验和失败而非复用。代价:retained 高(206MB,所有后端最高)+ quarantine 管理开销。
  3. per-thread 独占 TSDTSDRegistryExTscudo/tsd_exclusive.h:31pthread_key_create)——类似 jemalloc 的 per-thread tcache,但每次访问多一层 pthread_getspecific。在 shared_churn 中 scudo 30.4 ns vs jemalloc 16.7 ns:TSD 模型相同但 quarantine+校验和多了一层开销。
  4. 大块瓶颈:scudo pair_1mb = 8298 ns,是 jemalloc(423) 的 19.6×。根因:secondary cache 的 SecondaryCacheDefaultMaxEntrySize=512KBscudo/allocator_config.h:94),1MB 块绕过缓存直接 mmap/munmap——每次大块 free 都是一次内核 syscall。jemalloc/mimalloc 的大块缓存(death-row / large cache)避免了反复 mmap。
  5. retained 最高:206MB(vs libmalloc 17MB)。release interval = INT32_MAX(scudo/allocator_config.h:88,默认不释放),加 quarantine 保留延迟释放的块,加 secondary cache 缓存——三重保留。
  6. DarwinConfig 调参:DefaultConfig 的 PrimaryRegionSizeLog=32(4GB/region × ~45 size class = 180GB 虚拟预留),iOS 内核拒绝("internal map failure NO MEMORY requesting 188743680KB")。改 PrimaryRegionSizeLog=24(16MB/region,~720MB 总预留)后通过——这是 Darwin 移植的真实坑。
为什么 Android 选 scudo 而非 jemalloc:scudo 的 quarantine+checksum+guard page 是针对 Android 生态(恶意应用、NDK native 代码不可信)的安全防线。2.9× 速度税换"堆腐败/UAF 可检测"——在 Android 安全模型里值得。iOS 不需要 scudo 因为:① App 沙箱 + 代码签名(native 代码可控);② libmalloc 自带 checksum+guard page(magazine_tiny.c:free_list_checksum_ptr);③ jetsam 协同保命比分配器安全更重要。→ iOS 选 libmalloc(安全+保命协同),Android 选 scudo(纯安全加固)——不同威胁模型决定不同选择

1.5 libmalloc vs scudo 安全性源码级对比

两者都有安全加固,但加固的对象和层次完全不同

安全维度libmalloc(iOS)scudo(Android)源码依据
校验和保护对象free list 指针(链表节点)每 chunk header(每个块头)libmalloc: magazine_inline.h:194 / scudo: chunk.h:122
校验和强度arm64e: PAC 全签名 / 非 arm64e: 4-bit16-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_HOOKSscudo/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
ASLRDISABLE_ASLRPrimaryEnableRandomOffsetmagazine_malloc.c:1707
释放后内存毒化PatternOrZeroFill 0xABscudo/common.h:203
释放栈追踪storeDeallocationStackMaybe(MTE 时)scudo/combined.h:1149
VM tag✅ Darwin 原生❌ Linux 无此机制magazine_malloc.c

libmalloc 的安全模型:保护元数据 + 信任平台沙箱

libmalloc 的加固哲学:"保护 free list 不被腐败,其余交给平台沙箱"

① free list 指针校验和——两条路径magazine_inline.h:190-253):

② 没有 UAF 检测:free → 直接链入 free list → 立即可复用。没有 quarantine、没有 State 追踪。UAF 在复用窗口内被静默吞掉。

③ 没有 double-free 检测:第二次 free 只是把指针再链入(如果 checksum 过了)。没有 State 字段说"这个块已经是 Available 了"。

scudo 的安全模型:每块全生命周期追踪

scudo 的加固哲学:"每个 chunk 从分配到释放到回收都有 header 追踪 + CRC32 校验 + quarantine 延迟"

① 每 chunk header 有 State + CRC32chunk.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:30combined.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 做类似的事。

一句话:libmalloc 的安全是"保护元数据(free list 指针)+ 信任平台沙箱";scudo 的安全是"不信任任何东西,每块全生命周期追踪"。对 free list 篡改这个具体威胁,arm64e 的 PAC 比 scudo CRC32 更强;对 UAF/double-free/overflow 这些威胁,scudo 的 quarantine+State+MTE+GWP-ASan 是 libmalloc 没有的——但 iOS 由代码签名 + 沙箱 + bmalloc 在平台层消化了这些威胁。

1.6 为什么 iOS 不用 scudo 替代 libmalloc

scudo 确实比 libmalloc 快(60 vs 88 ns),安全机制也更全。但"快+安全"不是 iOS 不换 scudo 的原因——libmalloc 有 4 件 scudo 架构层面做不到的事,其中第 1 件是 iOS 流畅性的命根

① jetsam 协同链(命根——scudo 完全没有)

这是 iOS App 后台能保命的核心。libmalloc 把"何时还内存"的决策权下放给内核,scudo 架构里没有这个通道:

libmalloc 协同机制scudo 对应物差距
MADV_FREE_REUSABLE(Darwin 专属:"我先用着但压力来时让给你")MADV_DONTNEED(Linux:"我不要了")语义完全不同——REUSABLE 让内核优先回收而非杀进程
收到 memorystatus 压力事件 → malloc_memory_event_handlerpressure_relief 主动 flush❌ 没有被动响应内核压力的机制scudo 只定时 decay,不会在 jetsam 发信号时主动还
VM_MAKE_TAG 让 jetsam 按任务定位大消耗者❌ Linux 无 VM tagjetsam 无法精确定位"哪个 App 哪类分配最耗内存"
vm_purgable_control 可清除分配❌ scudo 无此抽象WebKit 的 purgeable image 靠这个

关键反证:把 libmalloc 换成 scudo → App 后台时 scudo 不响应 jetsam 压力信号 → jetsam 看到占着 RAM 不还 → 直接杀进程。这是 iOS 不能接受的结果——流畅性的第一支柱"进程还在"就塌了。

② arm64e PAC 比 scudo CRC32 更强(对 free list 篡改这个具体威胁)

scudo 的 16-bit CRC 可碰撞;libmalloc arm64e 用 ptrauth_sign 硬件密码学签名。对 free list 篡改这一个威胁,iOS 已有比 scudo 更强的方案。scudo 多出的 quarantine/MTE/GWP-ASan 防的是 UAF/overflow——这些在 iOS 上由代码签名 + 沙箱 + WebKit bmalloc 在平台层消化了。

③ per-CPU 杂志——固定核数硬件的最优 cache 局部性

Apple 既造 SoC(A14=6 核)又出 OS,已知固定核数。per-CPU 杂志的锁 cache line 永远只被同一核碰——不跨核弹跳。scudo 的 per-thread TSD(pthread_key_create)每次访问多一层 pthread_getspecific,线程多时 tcache 膨胀。

④ 25 年深度集成

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 集成。

一句话回答:iOS 不用 scudo 不是因为 scudo 不好,而是因为 libmalloc 做的 4 件事里有 1 件(jetsam 协同)是 iOS 流畅性的命根,而 scudo 架构层面没有这个通道。你要让 scudo 做 MADV_FREE_REUSABLE + pressure_relief + VM tag + vm_purgable_control,就得把 libmalloc 的 Darwin 平台层全部移植过去——那不是"优化 scudo",那是"重写 libmalloc"。两套方案各自是各自生态的局部最优,不是同一个问题的两种解法。
"scudo 能更快"的错觉:scudo 关掉 quarantine 会更快,但关掉 quarantine 的 scudo 就不是 scudo 了——退化成"有 CRC 校验的普通分配器",不如直接用 jemalloc(21 ns,比关 quarantine 的 scudo 还快)。scudo 的安全价值全在 quarantine+MTE+GWP-ASan,这些全是性能开销——打开 = 60 ns(比 jemalloc 慢 2.9×);关掉 = 丢了安全优势。scudo 不能"既安全又快"——它的安全就是用慢换的

2. 源码级对比:libmalloc vs jemalloc

2.1 快路径定位:per-CPU vs per-thread(核心差异)

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 调度下低频。

2.2 快路径是否持锁:持锁 vs 无锁(~88ns vs ~21ns 根因)

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)。

2.3 回收策略:pressure 驱动 vs decay 定时

libmalloc:region 进 depot 后才逐页 MADV_FREE_REUSABLEmagazine_tiny.c:839pinned_to_depot 保证锁外 madvise 安全)。回收是 pressure/recirc 驱动;大块走 MADV_REUSABLE death-row cache。

jemallocdecay_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 tagmmap
回收depot 逐页 MADV_FREE_REUSABLE,pressure 驱动decay 定时,可调
安全加固校验和 + guard pages默认无

3. libmalloc 设计思想与 jetsam 协同链

3.1 设计思想

源码文件头注释(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 硬件上的特化,四个核心思想:

  1. per-CPU 杂志而非 per-thread:Apple 既造 SoC 又出 OS,已知固定核数。在固定小核数硬件上缓存局部性最优、无 false sharing,无需为任意线程数动态建/毁 tcache。
  2. RAM 是瓶颈而非 CPU:4-6GB 手机上 retained → 被 jetsam 杀 → App 死。回收与内核压力信号耦合(reactive),而非 jemalloc 定时 decay("eventually 还"在突发压力下来不及)。把"何时还内存"决策权交给内核——它知道全局压力。
  3. 安全优先:手机跑不可信 Web 内容(WebKit),堆加固比纳秒更重要 → 校验和指针 + guard page。
  4. 原生集成:用 Darwin 独有的 VM-tag / MADV_FREE_REUSABLE / vm_purgable_control——这是 mmap-only 的 jemalloc/mimalloc 拿不到的协同通道。

3.2 jetsam 协同链(双层:主动提示 + 被动释放)

A. 主动提示层(常开——让内核优先回收 libmalloc 的页,而非杀进程)

  1. VM tag:每次 mach_vm_allocate(... VM_MAKE_TAG(VM_MEMORY_MALLOC_TINY))libmalloc/vm.c:89)。jetsam 杀选启发式按 per-task VM tag 找大消耗者。
  2. MADV_FREE_REUSABLECONFIG_MADVISE_STYLE):depot 中 free span 走 MADV_FREE_REUSABLEvm.c:463,注释 :467 确认)。Darwin 语义:标记页可复用 → 内核压力下优先回收这些页,先于杀进程。libmalloc 的"已释放但缓存"页是全系统最便宜的 RAM 回收源。
  3. Large death-rowlarge_entry_cache):大块 free → LIFO 进缓存 + MADV_REUSABLE;内核可回收,下次大块 alloc 直接复用。
  4. Purgeablevm_purgable_control(VM_PURGABLE_SET_STATE)):可清除分配 → 内核单方面清空。

B. 被动释放层(收到压力信号即 flush)——完整调用链

内核 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 而无需杀进程

C. 可观测层(供 jetsam/diagnostics 安全读他任务 zone)

scalable_zone_info_task(task, memory_reader_t reader, ...)magazine_malloc.c:954):跨任务(用 memory_reader_t)安全读 malloc zone 统计。

4. 为什么 libmalloc 慢但 iOS 流畅

bench 证 libmalloc 慢 4-6×,但手里的 iPhone 依然丝滑。因为分配器快路径延迟根本不是流畅性的瓶颈——流畅性是一个系统级指标,由完全不同的因素决定。

4.1 绝对量级:不是瓶颈

libmalloc 一次 32B malloc+free = ~88ns。一个 App 每帧(16.6ms@60Hz)做 10 万次 alloc/free(已是很重的逻辑):100,000 × 88ns = 8.8ms < 16.6ms,不掉帧。jemalloc 同负载省下的 6.7ms 在 16.6ms 帧预算里省不出肉眼可感区别。

4.2 流畅性真正依赖的 5 件事,libmalloc 全做对了

  1. 内存不膨胀 → 不被 jetsam 杀(最关键):libmalloc retained 低(17MB)+ jetsam 协同,App 后台能保命、前台不重启。jemalloc 默认(81MB)同压力下更早被杀 → 切回前台冷启动 → 真正可感的卡顿。流畅的前提是"进程还在"。
  2. UI 在独立高优先级线程,不与 App 逻辑争 malloc:iOS 渲染是 Core Animation + Render Server(独立进程 backboardd),App 只提交 layer 树。App malloc 慢只影响逻辑线程,Render Server 不用 App 的 malloc → 渲染帧率不受影响。
  3. UIKit 用对象池/缓存,规避了快路径:UICollectionView/TableView 的 cell 复用、UIImage 缓存——高频 UI 对象根本不走 malloc/free 循环,一次 dequeue 是查表不是分配。
  4. ARC + autorelease pool 的释放是批量的:ARC 释放延迟批量(autoreleasepool drain 一次性 free),不是紧耦合 alloc/free pair。libmalloc 的单槽 cache 在批量复用时命中率高。bench 的 pair_32b 是最不利于 libmalloc 命中率的模式——真实 App 没这么用。
  5. A14 算力冗余 + 大核突发:Firestorm 大核 ~3GHz,88ns ≈ 260 周期,微秒级抖动被算力冗余吃掉。UI 线程最高优先级 + 大核绑定。

4.3 对照:Android 用 jemalloc 但仍卡?

Android Bionic 用 jemalloc(后换 Scudo),分配器更快。但 Android 卡顿来源是:后台进程多→RAM 竞争→LMK 频繁杀→切回冷启动;ART GC 停顿;RenderThread 与 App 共进程争用。Android 选择了快的分配器,输在了"内存/进程保命"。这反向印证:分配器快慢不是关键,内存策略协同才是。libmalloc 慢但协同好,iOS 反而更流畅。

一句话:iOS 流畅不是因为 libmalloc 快,是因为 App 不被杀(内存协同 > 分配速度)+ 渲染走独立流水线 + UI 对象池化 + 大核吸收抖动。libmalloc 用 4-6× 速度税换来"App 活着、渲染独立、对象不分配"——这才是流畅的真正支柱。bench 测的"分配速度"在流畅性方程里是二阶项

5. 内核 WKdm 多线程解压

5.1 源码结论:解压多 CPU 并发,压缩单线程

维度内核行为源码依据
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

5.2 实测(A14 / 6 核,每线程独立 dst+scratch 模拟 per-CPU)

模式压缩解压(单核)解压(6核并发)倍率
ALL_ZEROS~4500sv,无解压
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 核并发测)。

互证:TYPICAL 压缩比 0.369 == 设备 info.compalgo.ratio 0.37(内核累计 sysctl)——独立两条路径给出相同比率,强证据这是设备真实压缩器。

6. 鸿蒙 NEXT 对比:统一渲染 + memcg/zswapd

修正声明:作者最初按旧鸿蒙(App 进程内 RenderThread 栅格化 + Linux LMK 粗放杀)对比,被指正后核实 OpenHarmony 内核源码,两处结论大幅修正

6.1 统一渲染:渲染耦合从"iOS 略优"改为"持平"

旧鸿蒙鸿蒙 NEXT(统一渲染)iOS
栅格化App 进程内 RenderThreadRS 进程集中栅格化backboardd 进程
合成RS 进程RS 进程backboardd 进程
App 提交已栅格化图层渲染命令树(display list)layer 树快照

统一渲染把"栅格化"从 App 进程搬到 RS 进程——结构上向 iOS 的 backboardd 靠拢。App UI 线程卡顿不再直接卡栅格化,和 iOS"App 卡了当帧仍能渲染"同一机理。渲染耦合维度:撤销原"iOS 略优",改判持平。

6.2 内存保命:从"iOS 胜(鸿蒙靠 LMK 粗放杀)"改为"结构持平"

核实 OpenHarmony kernel v6.1 的 mm/ 目录,发现一整套 Huawei 自研、非 mainline 的内存子系统

OH 内核文件作用iOS 对应物
memcg_control.cper-app memcg 记账per-task VM tag + ledger
memcg_reclaim.cper-app 定向回收冷页(cgroup_reclaim(sc)jetsam 压力回收(libmalloc MADV_FREE_REUSABLE + flush)
zswapd.c内核线程kthread_run)做 RAM 内压缩交换到 zramvm_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 到闪存"是同一分层策略

修正:鸿蒙 NEXT 是 reclaim→compress→kill 分层回收(memcg 定向回收 + zswapd 压缩 + OOM 兜底杀),结构平行于 iOS,不是"粗放 LMK 杀进程"。内存保命维度:撤销原"iOS 胜",改判结构持平。

6.3 剩余真实差距(精细化)

维度iOS鸿蒙 NEXT判定
渲染耦合进程外 backboarddRS 集中栅格化+合成≈ 持平
内存保命jetsam+libmalloc 主动协同、vm_compressormemcg 定向回收+zswapd 压缩+zram↔UFS 分层≈ 结构持平
GC 停顿ARC 无 GCArkTS 仍有 GC(据称分代/并发)iOS 占优
范式负担保留模式+编译期+ARC声明式 diff+ArkCompiler AOTiOS 略优
算力冗余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 往接近无感调

7. iOS 的 memcg 对等物:per-task ledger + phys_footprint

iOS 没有 memcg(cgroups 子系统),但有完整功能对等物,且架构与 Linux memcg 在关键点上不同。XNU kern_memorystatus.c(5355 行)源码级核实:

7.1 iOS 的"memcg 对等物"组件

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

7.2 与 Linux memcg 的 5 点架构差异

① 记账指标:phys_footprint vs RSS+swap(iOS 更聪明)

Linux memcg 记账 RSS+swap+cache,可压缩的匿名页按原大小全额计入。iOS phys_footprint 把页拆成 internal_pages(未压缩)+ internal_compressed_pages(已压缩)分别记——压缩后 footprint 变小,App 看起来更省。iOS 的记账天然激励"压缩回收":压缩后 footprint 降、不触限;Linux memcg 除非开 zswap 记账否则激励不同。

② 分组单元:per-task vs cgroup

Linux memcg = 任意 cgroup 树,可多进程归一组共享预算、可嵌套、父子传播(灵活但 page_cgroup 每页挂钩开销大)。iOS = per-task(进程),Darwin 每任务天生有独立 vm_map,页天然归一 task,无需挂钩,再加优先级带归类。无"多进程共享预算"原生抽象,粒度粗但简单、零挂钩开销。

③ 限制:active/inactive 双限 vs 单 limit(iOS 为移动端调优)

Linux memcg:一个 memory.limit_in_bytes,超限触发回收。iOS:per-task 有 memlimit_active(前台)+ memlimit_inactive(后台)两套限(:973),加 FATAL/NONFATAL 标志(fatal=超限杀,nonfatal=只回收不杀)。前台 App 给大限、后台小限、自动切换——移动端专用调优,Linux 要靠 cgroup 切换+多 limit 模拟

④ 回收:系统级压缩 vs per-cgroup LRU(最根本架构差)

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_handlerpressure_relief → 主动 MADV_FREE_REUSABLE 自己的可回收页。iOS 把回收责任部分下放给分配器(libmalloc 主动提示),Linux memcg 模型里分配器只被动被回收——这是 iOS 一个真实但较小的精细度优势。

⑤ 杀:全局按优先级带 vs per-cgroup OOM

Linux memcg:cgroup 超 fatal 限 → 该 cgroup 内 OOM killer 杀一个。iOS:系统压力到阈值 → jetsam 按全系统优先级带从低到高杀(IDLE → AGING_BAND → ... → 前台最后),max_kill_priority(:909)控制杀到哪一带为止。iOS 杀是全局 App 优先级驱动(前台几乎不可能被杀,后台 IDLE 先死),Linux 是组内 OOM,与 App 重要性无关。

维度Linux memcgiOS(memorystatus+ledger+vm_compressor)
记账RSS+swap+cache(压缩页全计)phys_footprint(压缩页按压缩后计,分开)
分组任意 cgroup 树per-task + 优先级带
限额单 limitactive/inactive 双限 + fatal/nonfatal
回收per-cgroup 定向 LRU系统级压缩 + per-task 压力事件下放分配器
组内 OOM全局按 App 优先级带

9. 背景:Darwin 与 XNU 内核是什么

本文反复提到"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 的命令行工具(unamesysctlmach_vm_allocate 都是同一套)。本文测试设备 Darwin 20.6.0 = iOS 14.8(对应 macOS 11 Big Sur 内核版本号)。

9.1 XNU 内核 = Mach + BSD + I/O Kit

Darwin 的内核叫 XNU("X is Not Unix",递归缩写),是一个混合内核,三部分粘在一起跑在同一个内核地址空间:

组成来源职责
Mach 3.0卡内基梅隆大学微内核任务/线程/端口/消息(IPC)、虚拟内存、调度原语
BSD 层FreeBSD 衍生POSIX API(fork/exec/信号)、VFS 文件系统、BSD socket 网络、进程模型
I/O KitApple 自研C++ 设备驱动框架(显卡/USB/网络/存储驱动都基于它)

不是学术意义的微内核——Mach 没有跑在用户态,而是与 BSD 共享内核地址空间,是混合内核。

9.2 本文源码的归属

源码文件Darwin 层级本文用途
bsd/kern/kern_memorystatus.cBSD 层jetsam 优先级带 + per-task phys_footprint
osfmk/vm/vm_compressor.cMach 层WKdm 压缩器(解压 6 核并发)
osfmk/vm/vm_map.cMach 层phys_footprint 定义
libmalloc/magazine_tiny.clibSystem(用户态)per-CPU 杂志 + free list 校验和

9.3 为什么 scudo 需要"Darwin 移植"

scudo 上游只有三个平台层:linux.cpp(Linux futex、getrandom/proc/self/statm)、fuchsia.cpptrusty.cpp没有 Darwin 版——因为 Android/Fuchsia 不跑在 Darwin 上,Google 不会写。Darwin 的 API 跟 Linux 不同:

功能Linux(scudo 原用)Darwin(本项目移植)
互斥锁futex(SYS_futexos_unfair_lock
随机数getrandom syscallarc4random_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。

9.4 开源

Apple 在 github.com/apple-oss-distributions 开源了 Darwin 的内核(XNU)和核心库(libmalloc、Libc、libdispatch 等)。这就是能拉到 xnu-7195.141.2libmalloc-521.120.7 源码逐行核对的原因。但 GUI 层(AppKit/UIKit/Aqua)不开源。Darwin 是开源的;macOS/iOS 是闭源的商业产品 = Darwin + 闭源 GUI 框架。

10. 结论与方法论

核心论断分配器快路径延迟是流畅性的二阶项。真正的一阶项是:内存保命策略(reclaim→compress→kill 分层)+ 有无 GC + 渲染是否进程外 + 算力冗余
  1. iOS libmalloc 慢 4-6× 但 iOS 流畅——因为它的优化目标不在 bench 能测的"分配速度",而在"内存保命(jetsam 协同)+ 安全加固 + 固定核数可预测"。per-CPU 杂志 + MADV_FREE_REUSABLE + pressure_relief 让 App 后台能保命。
  2. jemalloc0(decay=0)速度最快且内存最省 → 综合最优(速度 21ns、retained 6MB)。decay 调参是 jemalloc 的免费午餐。mimalloc 调参反而是坏调参(不降 RSS 还拖垮大块)。
  3. per-CPU 杂志的锁局部性优势真实但不足以抵消"每操作两次锁"的固定开销——shared_churn 证伪了"跨线程流让 libmalloc 追平"的假设,gap 反从 4.2× 拉到 6.3×。jemalloc per-thread 无锁 tcache 在所有吞吐场景都赢。
  4. scudo(Android 11+ 默认)是"安全优先"的中间件:比 libmalloc 快 31%(60 vs 88 ns),比 jemalloc 慢 2.9×(60 vs 21 ns)。retained 最高(206MB)。大块瓶颈(8298 ns,secondary cache max=512KB 绕过缓存)。其 quarantine+checksum+MTE+GWP-ASan 是针对 Android 威胁模型的安全防线——iOS 不需要(沙箱+代码签名+libmalloc PAC+bmalloc),Android 需要(恶意应用+不可信 NDK)。不同威胁模型决定不同分配器选择
  5. iOS 不用 scudo 替代 libmalloc——不是 scudo 不好,而是 libmalloc 有 4 件 scudo 架构层面做不到的事:jetsam 协同链MADV_FREE_REUSABLE + pressure_relief 回调 + VM tag——scudo 只有 MADV_DONTNEED + 定时 decay,不响应内核压力信号,换上去 App 会被 jetsam 杀)、arm64e PAC(对 free list 篡改比 scudo CRC32 更强)、per-CPU 杂志(固定核数最优)、25 年深度集成。让 scudo 做 Darwin 协同 = 重写 libmalloc。两套方案是各自生态的局部最优。
  6. 鸿蒙 NEXT 在"渲染耦合"和"内存保命"两个一阶维度已结构性追平 iOS(统一渲染 + memcg/zswapd 分层回收),不再是"粗放 LMK 杀进程"。剩余 iOS 真实优势收窄为:无 GC(vs ArkTS GC)+ 算力统一冗余 + 分配器级主动回收提示。鸿蒙该补的是 ArkTS GC 而非分配器。
  7. iOS 没有 memcg,但 per-task ledger+phys_footprint+memlimit(active/inactive)+jetsam 带+vm_compressor 是其功能对等物,且在"压缩页分开记账""active/inactive 双限""全局压缩+分配器主动还""按 App 优先级杀"这几点上比 Linux memcg 更为移动端调过。三者 reclaim→compress→kill 都到位,分工哲学不同

方法论自省