zsmalloc 无锁定位:从 handle 到 class 零开销
源码版本:本文所有源码引用均基于 7da7f071,不同内核版本/分支的行号可能不同。
背景:这个问题从哪里来
zsmalloc 是内核中用于管理小尺寸内存对象的专用分配器,广泛用于 ZRAM、KVM 等场景。它通过 handle(一个 unsigned long 值)来标识已分配的对象,而非直接返回虚拟地址。分配时(zs_malloc)返回 handle,释放时(zs_free)传入 handle 即可。
然而,每次释放或读写对象时,都需要从 handle 定位到对应的 size_class(负责管理该对象所属的 size 类别,包括空闲链表、page 列表等)。如果这个定位过程需要加锁(例如保护 class 查找表或 zspage 的映射关系),在多核场景下会产生严重的锁竞争。
传统做法可能依赖全局锁来查找 class,这在高并发下效率低下。zsmalloc 的设计者选择了一种 无锁定位 方案:利用 handle 中编码的物理页信息,直接找到该对象所在的 zspage,而 zspage 结构体中预存了 class 索引。这样,在释放和读写的快路径中,不需要任何锁就能获得 class 指针,仅在该 class 内部的空闲链表操作时才需要 class 级锁。
核心机制与设计思路
1. Handle 的隐含信息
zsmalloc 分配的 handle 不是简单的 cookie,而是一个 encode 了物理页帧号(pfn)和对象在页内偏移的复合值。handle_to_obj() 从 handle 中提取出一个 obj(内部编码),然后 obj_to_location() 将该 obj 分解为 zpdesc(zsmalloc page descriptor)和 obj_idx(对象在 page 内的下标)。
关键点:从 handle 到 zspage 的转换完全基于已知的物理地址和 zspage 的组织结构,不依赖任何可变的状态或锁。这是因为 zpdesc 是通过 pfn 映射得到的,而 pfn 在对象生命周期内不会改变(除非发生 migration,但 migration 由 pool->lock 保护,且发生在慢路径)。
2. Class 索引的存储位置
每个 zspage 结构体中有一个字段 class,保存了它所属的 size_class 在 pool->size_class[] 数组中的下标。zspage_class() 函数利用这个下标直接从 pool 中取出 class 指针:
1 | static struct size_class *zspage_class(struct zs_pool *pool, |
这样,class 索引实际上被编码在了对象的物理环境中(通过 zspage),而不是显式地存储在 handle 的某个位域中。但 handle 通过 pfn 间接指向了 zspage,从而实现了无锁查找。
3. 无锁路径的保障
整个从 handle 到 class 的路径只涉及:
- 算术运算(提取 pfn、偏移)
- 页表/内存映射(通过 pfn 获取 page)
- 指针解引用(从 page 到 zpdesc 再到 zspage 再到 class)
这些操作都是 CPU 原子性的(读一个字),不涉及任何锁。只有在后续操作(如修改 free 链表、更新 fullness)时,才会获取 class->lock。因此,定位 class 本身是 lock-free 的。
以下示意图展示了 Handle 到 class 的数据流向:
1 | +---------------------+ +-------------------+ +-------------------+ |
1 | flowchart LR |
关键代码路径
释放路径:zs_free (mm/zsmalloc.c:1384)
1 | void zs_free(struct zs_pool *pool, unsigned long handle) |
关键的无锁步骤在 obj_to_location 和 get_zspage 中完成。zspage_class(mm/zsmalloc.c:457)直接通过数组下标查找,无锁。
读取路径:zs_obj_read_end (mm/zsmalloc.c:1087)
1 | void zs_obj_read_end(struct zs_pool *pool, unsigned long handle, |
同样的模式出现在 zs_obj_read_sg_begin、zs_obj_read_sg_end 等函数中。它们都依赖于 handle → obj → zpdesc → zspage → class 的无锁链。
分配路径中的编码:zs_malloc (mm/zsmalloc.c:1297)
虽然 zs_malloc 不直接使用 handle 中的 class 信息,但它创建 handle 时已经将 pfn 和偏移编码进去。zs_lookup_class_index(mm/zsmalloc.c:1021)基于 size 返回 class 下标,但这个下标并不直接存进 handle,而是存储在所属的 zspage 结构中。
与 Android/手机的关联
zsmalloc 是 ZRAM 的核心分配器,而 ZRAM 是 Android 系统压缩内存交换的标准方案。在 Android 手机上,ZRAM 负责将不活跃的匿名页压缩后存储,从而提升多任务并发能力。zsmalloc 的无锁 class 查找机制能减少高并发分配/释放场景的锁争用,直接提升了 ZRAM 的吞吐量和响应速度,对用户感知的流畅性有实际贡献。相关源码 mm/zsmalloc.c 是内核中 zsmalloc 的唯一实现,所有 Android 设备均依赖该模块。
延伸阅读
- Patch 系列的 lore 链接:本文基于 linux-next 7da7f071 的源码片段,但该特性并非新 patch,而是 zsmalloc 的长期设计。了解完整细节可阅读
mm/zsmalloc.c全文,尤其是handle_to_obj、obj_to_location和get_zspage的实现。 - 关于 zsmalloc 的整体设计,可参考内核文档
Documentation/mm/zsmalloc.rst(部分版本有)。 - 若对 ZRAM 性能优化感兴趣,可查阅与
CONFIG_ZSMALLOC_STAT相关的调试接口。