内存管理-60-zsmalloc-1-理论

一、mm/zsmalloc.c 文件的作用

mm/zsmalloc.c 是 Linux 内核里 zsmalloc 分配器 的实现文件,主要作用是给 zram/zswap 这类“压缩后小对象”场景提供高效内存分配。
一句话总结:它不是通用 kmalloc 替代品,而是专门为“可变长度压缩块”设计的内存池分配器。

1. 它解决什么问题

压缩后的页面大小不固定(可能 200B、1KB、3KB...),如果直接按页分配会浪费很大。
zsmalloc 的目标是:
(1) 把很多小压缩块紧凑放进物理页。
(2) 减少碎片。
(3) 支持快速分配/释放。
(4) 提供“句柄”访问,而不是长期直接映射指针。

2. 在系统里的位置关系

典型链路(zram场景):
(1) 匿名页被 swap out
(1) zram 压缩页数据
(1) 调用 zsmalloc 分配空间存放压缩数据
(1) 保存一个 handle(不是直接虚拟地址)
(1) 需要读回时再通过 handle 映射访问并解压
(1) 释放时把 handle 对应对象还给 zsmalloc

若进程被 kill 后,如果后端是 zram,最终就是走到 zsmalloc 把压缩对象回收。

3. 关键设计点

(1) 按 size class 管理。不同大小对象放到不同 class,减少内部碎片。
(2) zspage 概念。多个物理页组合成一个 zspage,便于容纳不同大小对象。
(3) handle 机制。外部拿到的是编码句柄,访问时临时 map/unmap,降低长期映射成本。
(4) 并发与局部性优化。有 per-class、per-cpu 等优化路径,减少锁竞争。

4. 为什么不直接用 slab/slub

slab/slub 更适合固定大小对象, 对“压缩后长度波动”利用率不理想。而 zsmalloc 用 size class + zspage + handle,让小对象装箱更紧凑,能显著降低 zram 物理占用和内部碎片,空间利用率更高。

5. 在调试时可关注:

(1) zram 物理占用上升但 swap 下降慢:可能释放路径或对象生命周期问题。
(2) 压缩率变化导致 class 分布变化:可能影响 zsmalloc 碎片率.
(3) 大量并发 swap in/out:关注 zsmalloc 路径锁竞争和延迟。
(4) zs_malloc 失败率是否上升(可能内存压力/碎片化问题)。
(5) 各 size_class 分布是否异常偏斜(压缩率变化信号)。
(6) zspage 回收是否及时(空页是否堆积)。
(7) map/unmap 热点是否引起锁竞争(高并发 swap in/out 场景)。

6. 典型调用流程(以 zram 为例)

(1) 压缩后得到一段可变长度数据(比如 1300B)。
(2) zs_malloc(pool, size) 选中对应 size_class。
(3) 在 class 的某个 zspage 里分配一个 slot,返回 handle。
(4) 写入压缩数据到该 slot。
(5) 读取时 zs_map_object(pool, handle, ...) -> 解压 -> zs_unmap_object(...)。
(6) 释放时 zs_free(pool, handle),slot 回收到 class。
(7) 若某个 zspage 空了,最终释放底层页。

7. 重点关注函数

zs_create_pool() / zs_destroy_pool() //池的生命周期。
zs_malloc() / zs_free() //对象分配与释放主入口。
zs_map_object() / zs_unmap_object() //通过 handle 映射对象内存进行读写。
find_get_zspage() //或同类命名函数, 从 class 中找可用 zspage。
obj_allocated() / obj_free() //或同类内部函数, slot 位图管理,标记已用/空闲。
free_zspage() //回收空的 zspage,释放底层页。


二、关键数据结构

1. struct zs_pool

整个 zsmalloc 池(一个池对应一个使用者,比如一个 zram 设备)。

2. struct size_class

按对象大小分级(例如 64B、96B、128B...)。每个 class 管一组同尺寸对象,减少碎片与查找成本。

3. struct zspage

zsmalloc 的核心容器,由 1 个或多个物理页组成。同一个 zspage 里只放某个 size_class 的对象。

4. unsigned long handle

外部不拿裸指针,而拿一个句柄(编码了对象位置)。访问对象时再 map/unmap,避免长期映射开销。


三、相关文档翻译

1. zsmalloc.rst

注: 翻译自 msm-5.4/Documentation/vm/zsmalloc.rst

这个分配器是为 zram 设计的。因此,它应当能够在低内存条件下良好工作。特别是,它从不尝试高阶页分配####,因为在内存压力下这类分配极有可能失败。另一方面,如果只使用单个(0 阶)页面,则会遭受非常严重的碎片问题,任何大小大于等于 PAGE_SIZE/2 的对象都会独占一个完整页面。这是其前身(xvmalloc)的主要问题之一。

为了解决这些问题,zsmalloc 会分配一组 0 阶页面,并通过多个 `struct page` 字段将它们链接在一起。这些链接后的页面会像一个高阶页一样工作,也就是说,一个对象可以跨越多个 0 阶页面的边界。代码中将这些链接在一起的页面整体称为`zspage`。####

为简化实现,zsmalloc 只能分配大小不超过 PAGE_SIZE 的对象####,因为这已经满足了当前所有使用者的需求(在最坏情况下,页面不可压缩,因此会按“原样”存储,
也就是以未压缩形式保存)。对于超过该大小的分配请求,会直接返回失败(参见 `zs_malloc()`)。

此外,`zs_malloc()` 返回的不是一个可直接解引用的指针。
它返回的是一个不透明的句柄(`unsigned long`),其中编码了已分配对象的真实位置。
之所以采用这层间接方式,是因为 zsmalloc 不会永久映射 zspage;否则在 32 位系统上,
内核态映射所使用的虚拟地址空间很小,容易引发问题。因此,在使用这块已分配内存之前,
必须先通过 `zs_map_object()` 将对象映射出来,以获得可用指针;使用完成后,再通过
`zs_unmap_object()` 解除映射。

stat
====

启用 `CONFIG_ZSMALLOC_STAT` 后,可以通过``/sys/kernel/debug/zsmalloc/<user name>`` 查看 zsmalloc 的内部信息。下面是一个统计输出示例::

# cat /sys/kernel/debug/zsmalloc/zram0/classes

class  size  almost_full  almost_empty  obj_allocated  obj_used  pages_used  pages_per_zspage
...
9      176   0            1             186            129       8           4
10     192   1            0             2880           2872      135         3
11     208   0            1             819            795       42          2
12     224   0            1             219            159       12          4
...

各列介绍:
class: 索引
size: zspage 中存放的对象大小
almost_full: `ZS_ALMOST_FULL` 状态的 zspage 数量(见下文)
almost_empty: `ZS_ALMOST_EMPTY` 状态的 zspage 数量(见下文)
obj_allocated: 已分配对象总数.
obj_used: 已经分配给用户使用的对象数量.
pages_used: 该 class 已分配的页面数量.
pages_per_zspage: 组成一个 zspage 所需的 0 阶页面数量.

当 `n <= N / f` 时,我们将一个 zspage 归入 `ZS_ALMOST_EMPTY` fullness分组,其中:
* `n` = 已分配对象数量
* `N` = 一个 zspage 能存放的对象总数
* `f` = `fullness_threshold_frac`(当前值为 4)

类似地,zspage 还会被归类为:

* `ZS_ALMOST_FULL`:当 `n > N / f`
* `ZS_EMPTY`:当 `n == 0`
* `ZS_FULL`:当 `n == N`

 

posted on 2026-03-28 11:45  Hello-World3  阅读(2)  评论(0)    收藏  举报

导航