zsmalloc · 压缩交换后端(zram)的对象分配器
进程 munmap
回收 zram 页
zs_free(obj)
每次释放必经
按对象大小分 size class
size class A
zspage 链
size class B
zspage 链
size class C
zspage 链
释放路径极热
Android:LMK 成批回收换出到 zram 的页
x86:zswap 重负载下同样如此
对象释放主导取消映射路径
多核并发时,2 处锁竞争主导延迟
每次释放要取 2 把锁
一次释放:池锁查 class,类锁释放整页
两把锁都串在同一条取消映射路径上
旧 zs_free:2 把锁串成一条链
obj 值
页帧号 + 对象偏移
读侧池锁
只为查 size class
类锁
持锁穿过整页释放
归还 buddy
还要取 zone 锁
2 次加锁,每次都串行
为什么要盯这 2 把锁?
Android LMK 成批回收换出到 zram 的页;x86 zswap 重负载同样频繁
对象释放主导取消映射路径,多核下这 2 处锁竞争主导延迟
两处竞争相互独立 → 两个修复各治一处
并发释放 · 池锁是读写锁,每个调用者都取读侧
CPU 0
CPU 1
CPU 2
CPU 3
read count · 全池共享的一条缓存行
缓存行来回抖
每个 CPU 只是「读」,却都要改同一个读计数
缓存行在并发释放之间反复抖动,成为瓶颈
类锁持锁穿过整页释放
类锁临界区:整页释放
zone 锁排队
同一个 size class 的其它释放:
CPU 0
CPU 1
CPU 2
全部被类锁挡住
改造:把 size class 索引编码进对象值
页帧号 pfn
class idx
offset
64 位机器字:pfn 之下还有富余位,拆成 class idx + offset
从 obj 值取 class idx
无锁,不取池锁
锁对应 size class
类锁内重读 obj 值
拿到稳定 pfn
再释放 zspage
页迁移只改写 pfn 字段,class idx 恒定不变 → 无锁读永远有效
读写 obj 值改用不可拆分的原语:读一次,不撕裂、不告警
32 位 / 个别 64 位回退配置:没有富余位
索引位宽折叠为零 → 编码自动失效,释放路径仍走原来的池锁
改前
类锁:摘下 zspage → 更新计数 → 整页释放
zone 锁的排队被关在类锁里面
改后
类锁:摘下 zspage + 更新计数
临界区到此为止
类锁外:整页释放
归还 buddy,这时才取 zone 锁
若摘下时该 zspage 正被别处占用?
交给延迟释放兜底;整页释放接口补了统一注释,交代适用时机
类锁不再覆盖 zone 锁,等待不再扩散到同 class 的其他 CPU
Raspberry Pi 4B · 4 核 ARM64 Cortex-A72
每进程 mmap 256MB → 写入 → MADV_PAGEOUT 经 zram(lzo-rle)换出 → 并发 munmap
single
59.0 ms
56.0 ms
提速
1.05x
multi 2p
94.6 ms
66.7 ms
提速
1.42x
multi 4p
202.9 ms
110.6 ms
提速
1.83x
同一套释放路径,并发越高收益越大
x86 · 20 核 Intel i7-12700 · 16 并发进程
同样的换出与 munmap 流程,只换机器
single
11.7 ms
9.8 ms
提速
1.19x
multi 2p
24.1 ms
17.2 ms
提速
1.40x
multi 4p
63.0 ms
45.3 ms
提速
1.39x
单线程改善有限:1.05x ~ 1.19x
消除的是并发竞争,不是单次开销
核数越多、并发越高越显著;RPi4 4 进程并发 1.83x,锁竞争才是多核瓶颈
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x