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_classpool->size_class[] 数组中的下标。zspage_class() 函数利用这个下标直接从 pool 中取出 class 指针:

1
2
3
4
5
static struct size_class *zspage_class(struct zs_pool *pool,
struct zspage *zspage)
{
return pool->size_class[zspage->class];
}

这样,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
2
3
4
5
6
7
8
9
10
11
+---------------------+      +-------------------+      +-------------------+
| Handle | ---> | obj | ---> | zpdesc |
| (unsigned long) | | (编码 pfn+偏移) | | (通过 pfn 找到) |
+---------------------+ +-------------------+ +-------------------+
|
| get_zspage()
v
+-------------------+ +-------------------+ +-------------------+
| class | <--- | zspage->class | <--- | zspage |
| (size_class *) | | (下标) | | |
+-------------------+ +-------------------+ +-------------------+
1
2
3
4
5
6
7
8
flowchart LR
A[handle 无符号整数] --> B[obj 内部编码]
B --> C[通过 pfn 得到 zpdesc]
C --> D[通过 zpdesc 得到 zspage]
D --> E[从 zspage 的 class 字段获取下标]
E --> F[在 pool->size_class 数组中找到 class 指针]
style A fill:#e1f5fe
style F fill:#c8e6c9

关键代码路径

释放路径:zs_free (mm/zsmalloc.c:1384)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void zs_free(struct zs_pool *pool, unsigned long handle)
{
struct zspage *zspage;
struct zpdesc *f_zpdesc;
unsigned long obj;
struct size_class *class;
int fullness;

if (IS_ERR_OR_NULL((void *)handle))
return;

// 从 handle 提取 obj 编码
obj = handle_to_obj(handle);
// 将 obj 转化为 zpdesc 和对象索引
obj_to_location(obj, &f_zpdesc, &obj_idx);
// 从 zpdesc 获取所属 zspage
zspage = get_zspage(f_zpdesc);
// 无锁获取 class
class = zspage_class(pool, zspage);
// 后续操作需要 class->lock,但获取 class 本身不持锁
...
}

关键的无锁步骤在 obj_to_locationget_zspage 中完成。zspage_classmm/zsmalloc.c:457)直接通过数组下标查找,无锁。

读取路径:zs_obj_read_end (mm/zsmalloc.c:1087)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void zs_obj_read_end(struct zs_pool *pool, unsigned long handle,
size_t mem_len, void *handle_mem)
{
struct zspage *zspage;
struct zpdesc *zpdesc;
unsigned long obj, off;
unsigned int obj_idx;
struct size_class *class;

obj = handle_to_obj(handle);
obj_to_location(obj, &zpdesc, &obj_idx);
zspage = get_zspage(zpdesc);
// 再次无锁获得 class(此处 class 仅用于校验或后续操作)
class = zspage_class(pool, zspage);
...
}

同样的模式出现在 zs_obj_read_sg_beginzs_obj_read_sg_end 等函数中。它们都依赖于 handle → obj → zpdesc → zspage → class 的无锁链。

分配路径中的编码:zs_malloc (mm/zsmalloc.c:1297)

虽然 zs_malloc 不直接使用 handle 中的 class 信息,但它创建 handle 时已经将 pfn 和偏移编码进去。zs_lookup_class_indexmm/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_objobj_to_locationget_zspage 的实现。
  • 关于 zsmalloc 的整体设计,可参考内核文档 Documentation/mm/zsmalloc.rst(部分版本有)。
  • 若对 ZRAM 性能优化感兴趣,可查阅与 CONFIG_ZSMALLOC_STAT 相关的调试接口。