基于 Linux 内核源码(主线 master 分支)实地分析
主要涉及文件:
mm/zswap.c(1845 行)mm/zsmalloc.c(2258 行)drivers/block/zram/zram_drv.c(3325 行)drivers/block/zram/zram_drv.hdrivers/block/zram/zcomp.c(258 行)drivers/block/zram/zcomp.h(97 行)drivers/block/zram/backend_lz4.c(128 行)drivers/block/zram/backend_zstd.c(218 行)drivers/block/zram/backend_lzo.c(60 行)
- 概述:三层压缩内存体系
- 整体架构图
- zsmalloc:专用压缩内存分配器
- 3.1 设计目标与约束
- 3.2 核心数据结构
- 3.3 size class 与 fullness group 机制
- 3.4 zspage 内存布局
- 3.5 handle 编码方案
- 3.6 对象读写:跨页映射问题
- 3.7 分配与释放路径
- 3.8 内存规整(compaction)
- zswap:交换前端压缩缓存
- 4.1 核心设计思路
- 4.2 核心数据结构
- 4.3 写入路径:zswap_store
- 4.4 读取路径:zswap_load
- 4.5 压缩/解压实现
- 4.6 Shrinker:内存压力淘汰
- 4.7 cgroup 感知机制
- 4.8 pool 管理与生命周期
- 4.9 初始化流程
- zram:压缩 RAM 块设备
- 5.1 定位与使用场景
- 5.2 核心数据结构
- 5.3 块设备注册与初始化
- 5.4 写入路径:zram_bio_write
- 5.5 读取路径:zram_bio_read
- 5.6 特殊页面处理
- 5.7 Writeback 到后备设备
- 5.8 多压缩算法支持(MULTI_COMP)
- 三者的关系与协作
- 锁机制分析
- 性能调优参数
- 统计与可观测性
- 常见问题与陷阱
- zswap 内存池演进:zbud、z3fold、zsmalloc
- zram 压缩算法深度剖析
- 12.1 zcomp 抽象层:统一压缩前端
- 12.2 lzo / lzo-rle 后端
- 12.3 lz4 / lz4hc 后端
- 12.4 zstd 后端(含字典支持)
- 12.5 deflate 与 842 后端
- 12.6 算法选择策略与对比
- zram 多流压缩:per-CPU 压缩流机制
- 13.1 per-CPU 流设计理念
- 13.2 zcomp_stream_get/put 实现
- 13.3 CPU 热插拔处理
- 13.4 压缩缓冲区布局
- zram 再压缩(recompress)机制
- 14.1 多优先级压缩设计
- 14.2 recompress_slot 执行流程
- 14.3 post-processing 控制结构
- zsmalloc handle 映射机制深度剖析
- 15.1 handle 间接寻址的完整链路
- 15.2 obj 值编码的位域结构
- 15.3 跨页对象的 kmap 策略
- 15.4 zs_obj_read_begin 接口
- zram writeback 深度分析
- 16.1 writeback 控制结构
- 16.2 异步 bio 提交与完成处理
- 16.3 compressed_wb 模式
- 16.4 访问时间追踪与 idle 标记
- zswap writeback 与驱逐策略深度分析
- 17.1 LRU 结构:list_lru 的 per-node、per-memcg 设计
- 17.2 shrinker 触发时机
- 17.3 shrink_worker 后台线程
- 17.4 writeback 并发安全分析
- 性能调优最佳实践与基准测试
- 18.1 zswap 调优指南
- 18.2 zram 调优指南
- 18.3 zsmalloc 调优
- 18.4 基准测试方法
- 18.5 常见部署场景配置
- 内核配置选项全览
- 附录:关键函数索引
Linux 内核中存在两条独立但可协作的压缩内存路径:
用途分类:
zswap -- 透明地拦截 swap out 路径,作为 swap 设备的前端缓存
zram -- 注册为一个块设备 /dev/zramN,用户配置为 swap 分区或 tmpfs
两者都使用 zsmalloc 作为底层的内存分配器来保存压缩后的数据。zsmalloc 专门为存储大量大小不一的压缩对象而设计,避免了 slab/slub 分配器对小对象的内碎片问题。
在内核 6.x 之前,zswap 还支持通过 zpool 抽象层连接 zbud(每页 2 对象)或 z3fold(每页 3 对象)后端。而在当前主线版本中,zswap 已经直接使用 zsmalloc(mm/zswap.c 第 154 行:struct zs_pool *zs_pool),zpool 接口已被移除。
进程 swap out 路径
(mm/page_io.c:275)
|
v
+-----+-------+ 命中:直接返回,无磁盘 IO
| zswap |<---------+
| (前端缓存) | |
+-----+-------+ |
| |
未命中 | 满了/淘汰 | swap_readpage
| |
v ^
+-----+-------+ swap in 路径
| swap 设备 | (zswap_load)
| (zram/ssd) |
+-------------+
进程直接使用
|
v
+-----+-------+
| zram | /dev/zramN
| 块设备驱动 | (可作为 swap 分区 / tmpfs)
+-----+-------+
|
v
+-----+-------+
| zsmalloc | 两者共同使用的底层分配器
| 内存分配器 |
+-------------+
进程访问内存
|
| 缺页异常 (do_swap_page)
v
+------------------+
| swap cache | (XArray: address_space->i_pages)
+------------------+
|
| swap_readpage (mm/page_io.c)
v
+------------------+ 命中 +------------------+
| zswap_load |-------->| 解压 -> 返回 |
+------------------+ | 无磁盘 IO |
| 未命中 +------------------+
v
+------------------+
| swap 设备读 | (真实磁盘/zram IO)
+------------------+
zs_pool
|
+-- size_class[0] (最小对象 ~32 字节)
| +-- fullness_list[ZS_INUSE_RATIO_0] (空闲 zspage)
| +-- fullness_list[ZS_INUSE_RATIO_10] (使用率 1-10%)
| +-- fullness_list[ZS_INUSE_RATIO_20]
| +-- ...
| +-- fullness_list[ZS_INUSE_RATIO_100] (全满 zspage)
|
+-- size_class[1]
| ...
|
+-- size_class[ZS_SIZE_CLASSES-1] (PAGE_SIZE,"huge" 对象)
zspage (每个 size_class 中的分配单元)
|
+-- first_zpdesc ----> zpdesc(page0) --> zpdesc(page1) --> ... --> NULL
|
+-- inuse (已分配对象数)
+-- freeobj (空闲对象链表头)
+-- fullness (当前 fullness group)
+-- class (所属 size_class 索引)
源码文件:mm/zsmalloc.c
zsmalloc 是专门为存储可变大小(通常是压缩后的页面数据)对象而设计的内存分配器。其核心挑战在于:
- 压缩后的对象大小通常在 50-3000 字节之间,分布不均
- 对象数量极多(每个 swap 页面对应一个对象)
- 要求内存利用率高(标准 slab 对小对象内碎片严重)
- 对象可能跨越物理页边界
关键设计约束(mm/zsmalloc.c 第 52-54 行):
#define ZS_ALIGN 8
#define ZS_HANDLE_SIZE (sizeof(unsigned long))
#define ZS_MAX_ALLOC_SIZE PAGE_SIZE最小分配大小(ZS_MIN_ALLOC_SIZE)至少为 32 字节,且必须是 ZS_ALIGN(8)的倍数。
struct zs_pool {
const char *name;
struct size_class *size_class[ZS_SIZE_CLASSES];
atomic_long_t pages_allocated;
struct zs_pool_stats stats;
/* Compact classes */
struct shrinker *shrinker;
/* protect zspage migration/compaction */
rwlock_t lock;
atomic_t compaction_in_progress;
};zs_pool 是 zsmalloc 的顶层句柄,包含最多 255 个 size_class(4K 页大小时),并有一个 rwlock 用于保护 zspage 的迁移操作。
struct size_class {
spinlock_t lock;
struct list_head fullness_list[NR_FULLNESS_GROUPS];
int size; /* 此 class 存储的对象大小(字节) */
int objs_per_zspage; /* 每个 zspage 可容纳的对象数 */
int pages_per_zspage; /* 每个 zspage 由几个物理页组成 */
unsigned int index;
struct zs_size_stat stats;
};每个 size_class 管理相同大小范围内的对象,并通过 fullness_list 数组按利用率分组管理 zspage。
struct zspage {
struct {
unsigned int huge:HUGE_BITS; /* 1bit: 是否为 huge 对象 */
unsigned int fullness:FULLNESS_BITS; /* 4bit: 利用率分组 */
unsigned int class:CLASS_BITS + 1; /* 9bit: size_class 索引 */
unsigned int magic:MAGIC_VAL_BITS; /* 8bit: ZSPAGE_MAGIC=0x58 */
};
unsigned int inuse; /* 已分配对象数 */
unsigned int freeobj; /* 空闲对象链表头(对象索引) */
struct zpdesc *first_zpdesc; /* 链表头:第一个物理页描述符 */
struct list_head list; /* fullness_list 中的链表节点 */
struct zs_pool *pool;
struct zspage_lock zsl; /* 自定义读写锁 */
};zspage 是 zsmalloc 的核心分配单元,将一个或多个物理页(通过 zpdesc 链表)聚合为一个逻辑分配区域。
struct link_free {
union {
unsigned long next; /* 空闲对象:下一个空闲对象的索引 */
unsigned long handle; /* 已分配对象:存储 handle */
};
};空闲对象内嵌入空闲链表指针,这是一种经典的侵入式空闲链表设计。
#define ZS_SIZE_CLASS_DELTA (PAGE_SIZE >> CLASS_BITS) /* 4K/256 = 16 字节 */
#define ZS_SIZE_CLASSES (DIV_ROUND_UP(ZS_MAX_ALLOC_SIZE - ZS_MIN_ALLOC_SIZE,
ZS_SIZE_CLASS_DELTA) + 1)以 4K 页为例,size class 步长为 16 字节,共约 255 个 class,覆盖从 32 到 4096 字节的全范围。
zs_create_pool(第 2059 行)从最大 class 向最小 class 倒序初始化,并执行 class 合并优化:当两个相邻 class 的 pages_per_zspage 和 objs_per_zspage 相同时,让较小的 class 复用较大的 class,减少不必要的内存碎片。
enum fullness_group {
ZS_INUSE_RATIO_0, /* 完全空闲 */
ZS_INUSE_RATIO_10, /* 使用率 1%-10% */
/* 8 个中间分组 ... */
ZS_INUSE_RATIO_99 = 10, /* 使用率 91%-99% */
ZS_INUSE_RATIO_100, /* 100% 满 */
NR_FULLNESS_GROUPS, /* = 12 */
};利用率计算(第 624-643 行):
static int get_fullness_group(struct size_class *class, struct zspage *zspage)
{
int inuse = get_zspage_inuse(zspage);
int objs_per_zspage = class->objs_per_zspage;
if (inuse == 0)
return ZS_INUSE_RATIO_0;
if (inuse == objs_per_zspage)
return ZS_INUSE_RATIO_100;
ratio = 100 * inuse / objs_per_zspage;
return ratio / 10 + 1; /* 向上取整修正:至少归入 RATIO_10 */
}分配时优先从非空的 fullness group 中取页,释放时动态调整 zspage 的 group(fix_fullness_group,第 683 行)。
以 size = 200 字节、pages_per_zspage = 2 为例:
物理页 0 (4096 字节) 物理页 1 (4096 字节)
+---------------------------+ +---------------------------+
| obj0: [handle(8B)][data] | | obj20: [handle][data] |
| obj1: [handle(8B)][data] | | obj21: ... |
| ... | | ... |
| obj19: [data 尾部跨页-----|--------> | ----data 头部] |
+---------------------------+ +---------------------------+
^ ^
first_zpdesc zpdesc->next
每个对象头部保留 ZS_HANDLE_SIZE(8 字节)用于存储 handle 指针,huge class(整页对象)例外,其 handle 直接存放在 zpdesc->handle 中。
对象可以横跨两个物理页(zs_obj_write 第 1207-1219 行处理此情况),这是 zsmalloc 最复杂的设计点,也是它需要 zspage_lock 的原因——防止在跨页读写时页面被迁移。
handle 本身是一个指向 slab 分配的内存的指针(handle_cachep 分配),该内存存储 obj 值(物理页帧号 + 页内对象索引的编码):
handle (unsigned long, 指针) --> 4/8 字节内存
存储 obj 编码值:
obj[63:OBJ_INDEX_BITS] = PFN (物理页帧号)
obj[OBJ_INDEX_BITS-1:0] = 对象在页内的索引
关键函数(第 721-751 行):
// obj 值 -> (物理页描述符, 对象索引)
static void obj_to_location(unsigned long obj, struct zpdesc **zpdesc,
unsigned int *obj_idx)
{
*zpdesc = pfn_zpdesc(obj >> OBJ_INDEX_BITS);
*obj_idx = (obj & OBJ_INDEX_MASK);
}
// (物理页描述符, 对象索引) -> obj 值
static unsigned long location_to_obj(struct zpdesc *zpdesc, unsigned int obj_idx)
{
unsigned long obj;
obj = zpdesc_pfn(zpdesc) << OBJ_INDEX_BITS;
obj |= obj_idx & OBJ_INDEX_MASK;
return obj;
}
// handle -> obj 值(解引用 handle 指针)
static unsigned long handle_to_obj(unsigned long handle)
{
return *(unsigned long *)handle;
}这种间接寻址设计允许在内存规整(compaction)时移动对象的物理位置,只需更新 handle 指向的 obj 值,所有持有 handle 的调用方不受影响。
void zs_obj_write(struct zs_pool *pool, unsigned long handle,
void *handle_mem, size_t mem_len)
{
// 1. 加 pool->lock 读锁,从 handle 定位到 zspage
read_lock(&pool->lock);
obj = handle_to_obj(handle);
obj_to_location(obj, &zpdesc, &obj_idx);
zspage = get_zspage(zpdesc);
// 2. 加 zspage 读锁,防止迁移
zspage_read_lock(zspage);
read_unlock(&pool->lock);
// 3. 计算对象在页内的偏移
off = offset_in_page(class->size * obj_idx);
if (!ZsHugePage(zspage))
off += ZS_HANDLE_SIZE; /* 跳过 handle 存储区 */
// 4. 判断是否跨页
if (off + mem_len <= PAGE_SIZE) {
/* 完全在单页内:kmap + memcpy */
void *dst = kmap_local_zpdesc(zpdesc);
memcpy(dst + off, handle_mem, mem_len);
kunmap_local(dst);
} else {
/* 跨越两页:分两段 memcpy */
sizes[0] = PAGE_SIZE - off;
sizes[1] = mem_len - sizes[0];
memcpy_to_page(zpdesc_page(zpdesc), off, handle_mem, sizes[0]);
zpdesc = get_next_zpdesc(zpdesc);
memcpy_to_page(zpdesc_page(zpdesc), 0,
handle_mem + sizes[0], sizes[1]);
}
zspage_read_unlock(zspage);
}读路径使用 scatter-gather 接口,返回 1 或 2 个 scatterlist 条目以避免额外的内存拷贝:
void zs_obj_read_sg_begin(struct zs_pool *pool, unsigned long handle,
struct scatterlist *sg, size_t mem_len)
{
// ...定位到 zspage,加读锁...
if (off + mem_len <= PAGE_SIZE) {
sg_init_table(sg, 1);
sg_set_page(sg, zpdesc_page(zpdesc), mem_len, off);
} else {
/* 跨两页,返回 2 个 SG 条目 */
sizes[0] = PAGE_SIZE - off;
sizes[1] = mem_len - sizes[0];
sg_init_table(sg, 2);
sg_set_page(sg, zpdesc_page(zpdesc), sizes[0], off);
zpdesc = get_next_zpdesc(zpdesc);
sg = sg_next(sg);
sg_set_page(sg, zpdesc_page(zpdesc), sizes[1], 0);
}
/* 注意:zspage 读锁在 zs_obj_read_sg_end 时释放 */
}这一设计使解压缩操作可以直接在 zspage 的物理页上进行 DMA 或 scatter 操作,避免中间拷贝。
unsigned long zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
const int nid)
{
// 1. 从 handle_cachep 分配 handle(slab 分配,不可在原子上下文中 sleep)
handle = cache_alloc_handle(gfp);
// 2. 加上 ZS_HANDLE_SIZE,找到对应的 size_class
size += ZS_HANDLE_SIZE;
class = pool->size_class[get_size_class_index(size)];
// 3. 在 class->lock 下查找非满的 zspage
spin_lock(&class->lock);
zspage = find_get_zspage(class);
if (likely(zspage)) {
obj_malloc(pool, zspage, handle);
fix_fullness_group(class, zspage); /* 可能需要移动到其他 fullness list */
class_stat_add(class, ZS_OBJS_INUSE, 1);
goto out;
}
spin_unlock(&class->lock);
// 4. 没有可用 zspage,新分配一个
zspage = alloc_zspage(pool, class, gfp, nid);
spin_lock(&class->lock);
obj_malloc(pool, zspage, handle);
// 更新统计、插入 fullness list、标记为可迁移...
out:
spin_unlock(&class->lock);
return handle;
}void zs_free(struct zs_pool *pool, unsigned long handle)
{
read_lock(&pool->lock); /* 防止 compaction 期间页面移动 */
obj = handle_to_obj(handle);
obj_to_zpdesc(obj, &f_zpdesc);
zspage = get_zspage(f_zpdesc);
class = zspage_class(pool, zspage);
spin_lock(&class->lock);
read_unlock(&pool->lock);
class_stat_sub(class, ZS_OBJS_INUSE, 1);
obj_free(class->size, obj); /* 将对象归还到 zspage 的空闲链表 */
fullness = fix_fullness_group(class, zspage);
if (fullness == ZS_INUSE_RATIO_0)
free_zspage(pool, class, zspage); /* zspage 全空时归还物理页 */
spin_unlock(&class->lock);
cache_free_handle(handle); /* 释放 handle slab 对象 */
}zsmalloc 实现了内部规整(CONFIG_COMPACTION),通过注册 shrinker(第 209 行:struct shrinker *shrinker)来驱动。规整将稀疏 zspage 中的对象迁移到更密集的 zspage,然后释放空出来的物理页。
规整时需要更新 handle(将 obj 值写入 handle 指向的内存),这正是间接 handle 设计的价值所在。规整操作受 pool->lock(写锁)和 zspage->zsl(写锁)双重保护,因此正常的读写路径持有读锁即可并发执行。
源码文件:mm/zswap.c
zswap 位于 swap out 路径和 swap 设备之间,作为一个透明的 RAM 缓存(第 3-11 行注释):
zswap is a cache that takes pages that are in the process of being swapped out and attempts to compress and store them in a RAM-based memory pool. This can result in a significant I/O reduction on the swap device and, in the case where decompressing from RAM is faster than reading from the swap device, can also improve workload performance.
关键设计决策:
- zswap 不拦截 swap in(读取)——swap in 时通过
zswap_load直接从 RAM 解压,无需磁盘 IO - 当内存压力大时,通过 shrinker 将 zswap 中的旧条目 writeback 到 swap 设备,为新条目腾出空间
- 使用 xarray(而非之前版本的红黑树)作为索引结构,key 为 swap 偏移量
- per-memcg LRU:支持 cgroup 级别的内存限制和淘汰策略
struct zswap_entry {
swp_entry_t swpentry; /* 对应的 swap 条目(类型 + 偏移量) */
unsigned int length; /* 压缩后的数据长度(字节);
== PAGE_SIZE 表示未压缩(不可压缩页面) */
bool referenced; /* 最近是否被访问,用于 shrinker 二次机会算法 */
struct zswap_pool *pool; /* 所属 zswap_pool */
unsigned long handle; /* zsmalloc handle,指向压缩数据存储位置 */
struct obj_cgroup *objcg; /* cgroup 计费对象 */
struct list_head lru; /* zswap_list_lru 中的 LRU 节点 */
};每个 zswap_entry 对应一个被压缩的 4K 页面。length == PAGE_SIZE 表示该页不可压缩,数据原样存储(仍存于 zsmalloc 中,占用约一个物理页)。
struct zswap_pool {
struct zs_pool *zs_pool; /* zsmalloc 池 */
struct crypto_acomp_ctx __percpu *acomp_ctx; /* 每 CPU 压缩上下文 */
struct percpu_ref ref; /* 引用计数(percpu 优化) */
struct list_head list; /* zswap_pools 全局链表 */
struct work_struct release_work; /* 异步销毁 workqueue item */
struct hlist_node node; /* cpuhp 注册节点 */
char tfm_name[CRYPTO_MAX_ALG_NAME]; /* 压缩算法名称(如 "lz4") */
};一个 zswap_pool 绑定一种压缩算法。切换压缩算法时会创建新 pool,旧 pool 引用归零后异步销毁。
struct crypto_acomp_ctx {
struct crypto_acomp *acomp; /* 异步压缩算法实例 */
struct acomp_req *req; /* 预分配的压缩请求 */
struct crypto_wait wait; /* 同步等待完成的等待对象 */
u8 *buffer; /* PAGE_SIZE 大小的临时缓冲区 */
struct mutex mutex; /* 保护此 per-CPU 上下文 */
};每个 CPU 有独立的 crypto_acomp_ctx,避免跨 CPU 竞争。压缩请求虽然是异步 API,但在 zswap 中通过 crypto_wait_req 同步等待完成(第 885 行)。
static struct xarray *zswap_trees[MAX_SWAPFILES];
static unsigned int nr_zswap_trees[MAX_SWAPFILES];每个 swap 类型(最多 MAX_SWAPFILES 个)拥有一组 xarray,每个 xarray 覆盖 ZSWAP_ADDRESS_SPACE_PAGES(= 16384 个页面,即 64MB)的 swap 空间。这种分段设计避免了单个超大 xarray(第 229-235 行):
#define ZSWAP_ADDRESS_SPACE_SHIFT 14
#define ZSWAP_ADDRESS_SPACE_PAGES (1 << ZSWAP_ADDRESS_SPACE_SHIFT)
static inline struct xarray *swap_zswap_tree(swp_entry_t swp)
{
return &zswap_trees[swp_type(swp)][swp_offset(swp)
>> ZSWAP_ADDRESS_SPACE_SHIFT];
}写入路径在 mm/page_io.c:275 被调用:if (zswap_store(folio)),在实际写入 swap 设备前尝试压缩存入 zswap。
zswap_store(folio)
|
+-- 检查 zswap_enabled
|
+-- get_obj_cgroup_from_folio() -- 获取 memcg 对象
|
+-- obj_cgroup_may_zswap() -- 检查 memcg zswap 限额
|
+-- shrink_memcg() -- 如果当前 memcg 已满,先主动淘汰
|
+-- zswap_check_limits() -- 检查全局内存限制
|
+-- zswap_pool_current_get() -- 获取当前活跃 pool 的引用
|
+-- memcg_list_lru_alloc() -- 确保 LRU 链表已为此 memcg 初始化
|
+-- 对 folio 中每个 page:
| zswap_store_page(page, objcg, pool)
|
+-- count_vm_events(ZSWPOUT, ...)
zswap_store_page(page, objcg, pool)
|
+-- zswap_entry_cache_alloc() -- 从 zswap_entry_cache slab 分配 entry
|
+-- zswap_compress(page, entry, pool)
| |
| +-- acomp_ctx_get_cpu_lock() -- 获取当前 CPU 的压缩上下文(加互斥锁)
| |
| +-- sg_init_one(&input, page)
| |
| +-- crypto_acomp_compress() + crypto_wait_req() -- 同步压缩
| |
| +-- 如果压缩后 >= PAGE_SIZE 且 memcg 允许 writeback:
| | 原样存储(不可压缩页面,length = PAGE_SIZE)
| |
| +-- zs_malloc(pool->zs_pool, dlen, GFP_NOWAIT|...)
| |
| +-- zs_obj_write() -- 写入 zsmalloc
| |
| +-- entry->handle = handle; entry->length = dlen
|
+-- xa_store(swap_zswap_tree(swpentry), offset, entry, ...)
| 将 entry 插入 xarray(key = swap 偏移量)
|
+-- 更新引用计数、cgroup 计费、统计计数器
|
+-- zswap_lru_add(&zswap_list_lru, entry) -- 加入 LRU
|
+-- entry->referenced = true -- 初始标记为已引用
关键点:zs_malloc 使用 GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE 标志(第 907 行),避免在 swap out 路径上触发内存回收死锁。
int zswap_load(struct folio *folio)
{
swp_entry_t swp = folio->swap;
pgoff_t offset = swp_offset(swp);
bool swapcache = folio_test_swapcache(folio);
struct xarray *tree = swap_zswap_tree(swp);
struct zswap_entry *entry;
if (zswap_never_enabled())
return -ENOENT; /* zswap 从未启用,快速返回 */
/* 不支持 large folio(多页 swap)*/
if (WARN_ON_ONCE(folio_test_large(folio))) {
folio_unlock(folio);
return -EINVAL;
}
/* 从 xarray 查找 entry */
entry = xa_load(tree, offset);
if (!entry)
return -ENOENT; /* 未命中,返回让调用方去读磁盘 */
/* 解压缩到 folio */
if (!zswap_decompress(entry, folio)) {
folio_unlock(folio);
return -EIO;
}
folio_mark_uptodate(folio);
count_vm_event(ZSWPIN);
/* swapcache 路径:解压成功后删除 zswap 条目
* 避免同时有 zswap 和 swapcache 两份内存拷贝 */
if (swapcache) {
folio_mark_dirty(folio);
xa_erase(tree, offset);
zswap_entry_free(entry);
}
folio_unlock(folio);
return 0; /* 成功,folio 已解锁且标记为 uptodate */
}返回值语义:
0:命中并成功解压,folio 已解锁-ENOENT:未命中,folio 保持锁定,调用方继续走磁盘 IO 路径-EIO:命中但解压失败,folio 已解锁但未标记 uptodate(会发送 SIGBUS)
static bool zswap_compress(struct page *page, struct zswap_entry *entry,
struct zswap_pool *pool)
{
struct crypto_acomp_ctx *acomp_ctx;
struct scatterlist input, output;
unsigned int dlen = PAGE_SIZE;
unsigned long handle;
acomp_ctx = acomp_ctx_get_cpu_lock(pool); /* 获取当前 CPU 的上下文并加锁 */
dst = acomp_ctx->buffer; /* 每 CPU PAGE_SIZE 临时缓冲 */
sg_init_table(&input, 1);
sg_set_page(&input, page, PAGE_SIZE, 0);
sg_init_one(&output, dst, PAGE_SIZE);
acomp_request_set_params(acomp_ctx->req, &input, &output, PAGE_SIZE, dlen);
/* 异步 API,但用 crypto_wait_req 同步等待 */
comp_ret = crypto_wait_req(crypto_acomp_compress(acomp_ctx->req),
&acomp_ctx->wait);
dlen = acomp_ctx->req->dlen;
/* 压缩效果差(>= PAGE_SIZE)时的处理:
* 若 memcg 允许 writeback,原样存储(length = PAGE_SIZE);
* 否则拒绝(不浪费 zsmalloc 空间)*/
if (comp_ret || !dlen || dlen >= PAGE_SIZE) {
if (!mem_cgroup_zswap_writeback_enabled(...)) {
comp_ret = comp_ret ? comp_ret : -EINVAL;
goto unlock;
}
comp_ret = 0;
dlen = PAGE_SIZE;
dst = kmap_local_page(page); /* 指向原始页面数据 */
mapped = true;
}
/* 分配 zsmalloc 空间并写入 */
handle = zs_malloc(pool->zs_pool, dlen, GFP_NOWAIT|..., page_to_nid(page));
zs_obj_write(pool->zs_pool, handle, dst, dlen);
entry->handle = handle;
entry->length = dlen;
...
}static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)
{
struct scatterlist input[2]; /* zsmalloc 最多返回 2 个 SG 条目 */
struct scatterlist output;
struct crypto_acomp_ctx *acomp_ctx;
acomp_ctx = acomp_ctx_get_cpu_lock(pool);
/* 获取 zsmalloc 中存储数据的 SG 列表(可能跨页) */
zs_obj_read_sg_begin(pool->zs_pool, entry->handle, input, entry->length);
/* 未压缩页(length == PAGE_SIZE)直接 memcpy */
if (entry->length == PAGE_SIZE) {
memcpy_from_sglist(kmap_local_folio(folio, 0), input, 0, PAGE_SIZE);
dlen = PAGE_SIZE;
} else {
/* 标准解压缩路径 */
sg_init_table(&output, 1);
sg_set_folio(&output, folio, PAGE_SIZE, 0);
acomp_request_set_params(acomp_ctx->req, input, &output,
entry->length, PAGE_SIZE);
ret = crypto_wait_req(crypto_acomp_decompress(acomp_ctx->req),
&acomp_ctx->wait);
dlen = acomp_ctx->req->dlen;
}
zs_obj_read_sg_end(pool->zs_pool, entry->handle);
...
}注意 input 是 SG 数组(最多 2 个条目,对应跨页对象),这样解压缩时可以直接从 zsmalloc 的物理页读取,无需先复制到连续内存。
zswap 的 shrinker 实现了一个精心设计的二次机会(second chance)算法,同时考虑 swap in 速率和压缩比。
static enum lru_status shrink_memcg_cb(struct list_head *item,
struct list_lru_one *l, void *arg)
{
struct zswap_entry *entry = container_of(item, struct zswap_entry, lru);
/* 二次机会:若条目刚刚进入 zswap(referenced=true),
* 给它一次机会:清除标记并旋转到 LRU 尾部 */
if (entry->referenced) {
entry->referenced = false;
return LRU_ROTATE; /* 旋转,不淘汰 */
}
/* referenced=false:执行写回 */
list_move_tail(item, &l->list); /* 先旋转,失败时不用再放回 */
swpentry = entry->swpentry; /* 复制到栈,防止 entry 被并发释放 */
spin_unlock(&l->lock);
writeback_result = zswap_writeback_entry(entry, swpentry);
if (writeback_result == -EEXIST && encountered_page_in_swapcache) {
ret = LRU_STOP; /* 遇到 swapcache 中的页,说明在收缩热端,停止 */
*encountered_page_in_swapcache = true;
}
...
}static unsigned long zswap_shrinker_count(struct shrinker *shrinker,
struct shrink_control *sc)
{
/* 获取可收缩条目数 */
nr_freeable = list_lru_shrink_count(&zswap_list_lru, sc);
/* 减去最近的 disk swap in 次数
* (说明我们在过度收缩,应该保留更多条目) */
nr_freeable -= nr_disk_swapins_cur - nr_remain;
/* 按压缩比缩放:压缩比越好,每次写回节省越少内存,
* 因此减少建议的回收数量 */
return mult_frac(nr_freeable, nr_backing, nr_stored);
// ^可回收条目 ^实际占用页 ^存储条目数
// 例:若压缩比 = 3:1,则 nr_backing/nr_stored = 1/3,
// 返回 nr_freeable/3,即每回收 3 个条目才节省约 1 个页
}这一策略有效防止了在高压缩比工作负载下对 zswap 的过度淘汰。
zswap_writeback_entry(entry, swpentry)
|
+-- get_swap_device() -- 确保 swap 设备仍然有效
|
+-- swap_cache_alloc_folio() -- 分配 swap cache folio
|
+-- 验证 entry 仍在 xarray 中(防止并发失效)
|
+-- zswap_decompress(entry, folio) -- 解压到 folio
|
+-- xa_erase(tree, offset) -- 从 xarray 删除 entry
|
+-- zswap_entry_free(entry) -- 释放 zsmalloc 空间
|
+-- folio_mark_uptodate(folio)
|
+-- __swap_writepage(folio, NULL) -- 异步写入 swap 设备
zswap 支持完整的 cgroup v2 感知(CONFIG_MEMCG):
- per-memcg 计费:
obj_cgroup_charge_zswap(objcg, entry->length)在压缩成功时收费,obj_cgroup_uncharge_zswap在释放时退费(第 722-724 行) - per-memcg LRU:
zswap_list_lru使用list_lru_init_memcg初始化,支持 per-node、per-memcg 的独立 LRU(第 1798 行) - memcg 限制检查:
obj_cgroup_may_zswap(objcg)检查此 memcg 的 zswap 使用量是否超限 - memcg writeback 控制:
memory.zswap.writebackcgroup 接口可以禁用特定 memcg 的 zswap writeback,对应mem_cgroup_zswap_writeback_enabled()函数 - memcg 下线清理:
zswap_memcg_offline_cleanup(第 682 行)在 memcg 下线时推进 shrinker 游标
统计计数器(第 1227-1231 行):
if (!mem_cgroup_disabled()) {
mem_cgroup_flush_stats(memcg);
nr_backing = memcg_page_state(memcg, MEMCG_ZSWAP_B) >> PAGE_SHIFT;
nr_stored = memcg_page_state(memcg, MEMCG_ZSWAPPED);
} else {
nr_backing = zswap_total_pages();
nr_stored = atomic_long_read(&zswap_stored_pages);
}zswap 支持运行时切换压缩算法(通过写 /sys/module/zswap/parameters/compressor),这会触发新 pool 的创建和旧 pool 的淘汰。
/sys/module/zswap/parameters/compressor = "lz4"
|
v
zswap_compressor_param_set()
|
+-- 检查新算法是否可用(crypto_has_acomp)
|
+-- spin_lock(zswap_pools_lock)
|
+-- 查找是否有现有 pool 使用此算法(zswap_pool_find_get)
|
+-- 若没有:zswap_pool_create(s)
| |
| +-- zs_create_pool(name) -- 创建 zsmalloc pool
| +-- alloc_percpu(acomp_ctx) -- 分配 per-CPU 压缩上下文
| +-- cpuhp_state_add_instance() -- 注册 CPU hotplug 回调
| +-- percpu_ref_init() -- 初始化 percpu 引用计数
|
+-- list_add_rcu(&pool->list, &zswap_pools) -- 设为当前 pool
|
+-- percpu_ref_kill(&old_pool->ref) -- 开始淘汰旧 pool
|
v
__zswap_pool_empty() (ref 归零回调)
|
v
schedule_work(&pool->release_work)
|
v
__zswap_pool_release()
|
+-- synchronize_rcu()
+-- zswap_pool_destroy()
|
+-- zs_destroy_pool()
+-- free_percpu(acomp_ctx)
+-- kfree(pool)
zswap_setup()(第 1772-1833 行)在 late_initcall 阶段执行(确保加密子系统已就绪):
static int zswap_setup(void)
{
/* 1. 创建 entry slab 缓存 */
zswap_entry_cache = KMEM_CACHE(zswap_entry, 0);
/* 2. 注册 CPU hotplug 回调(管理每 CPU 压缩上下文) */
cpuhp_setup_state_multi(CPUHP_MM_ZSWP_POOL_PREPARE,
"mm/zswap_pool:prepare",
zswap_cpu_comp_prepare,
zswap_cpu_comp_dead);
/* 3. 创建 shrink 工作队列 */
shrink_wq = alloc_workqueue("zswap-shrink",
WQ_UNBOUND | WQ_MEM_RECLAIM, 1);
/* 4. 创建并注册 shrinker(NUMA-aware + memcg-aware) */
zswap_shrinker = zswap_alloc_shrinker();
list_lru_init_memcg(&zswap_list_lru, zswap_shrinker);
shrinker_register(zswap_shrinker);
/* 5. 注册 shrink_worker 到 work struct */
INIT_WORK(&zswap_shrink_work, shrink_worker);
/* 6. 创建默认压缩 pool(CONFIG_ZSWAP_COMPRESSOR_DEFAULT) */
pool = __zswap_pool_create_fallback();
/* 7. 初始化 debugfs */
zswap_debugfs_init();
zswap_init_state = ZSWAP_INIT_SUCCEED;
}
late_initcall(zswap_init);源码文件:drivers/block/zram/zram_drv.c,drivers/block/zram/zram_drv.h
zram 与 zswap 的根本区别在于:zram 是一个块设备,用户态可以像对待普通磁盘一样使用它。典型场景:
mkswap /dev/zram0 && swapon /dev/zram0 # 用作 swap 分区
mount -t tmpfs none /tmp -o size=2G # 配合 zram 用作 tmpfs
echo lz4 > /sys/block/zram0/comp_algorithm # 设置压缩算法
与 zswap 的关键差异:
- zswap 对用户透明,不需要配置 swap 设备;zram 需要显式挂载
- zswap 是 swap 设备的缓存层(两层存储);zram 是独立的压缩块设备(单层)
- zswap 在内核内存压力时自动回收;zram 压缩失败时直接报错
- 两者可以组合:zswap 在前,zram 在后(zswap → zram)
struct zram_table_entry {
unsigned long handle; /* zsmalloc handle;或特殊标志值 */
union {
unsigned long __lock; /* bit lock(ZRAM_ENTRY_LOCK 位) */
struct attr {
u32 flags; /* 低 ZRAM_FLAG_SHIFT 位:压缩数据大小;
高位:ZRAM 页面标志 */
#ifdef CONFIG_ZRAM_TRACK_ENTRY_ACTIME
u32 ac_time; /* 最近访问时间(boot 秒数) */
#endif
} attr;
};
struct lockdep_map dep_map; /* lockdep 支持 */
};标志位布局(zram_drv.h 第 39-58 行):
attr.flags 位域:
[ZRAM_FLAG_SHIFT-1 : 0] = 压缩后数据大小(最大 PAGE_SIZE = 2^PAGE_SHIFT 字节)
[ZRAM_FLAG_SHIFT] = ZRAM_SAME : 全同元素页(无需存储数据)
[ZRAM_FLAG_SHIFT+1] = ZRAM_ENTRY_LOCK : slot 锁 bit
[ZRAM_FLAG_SHIFT+2] = ZRAM_WB : 已写回到后备设备
[ZRAM_FLAG_SHIFT+3] = ZRAM_PP_SLOT : 已选为后处理候选
[ZRAM_FLAG_SHIFT+4] = ZRAM_HUGE : 不可压缩页(原样存储)
[ZRAM_FLAG_SHIFT+5] = ZRAM_IDLE : 自上次 idle 标记以来未访问
[ZRAM_FLAG_SHIFT+6] = ZRAM_INCOMPRESSIBLE : 任何算法都无法压缩
[ZRAM_FLAG_SHIFT+7] = ZRAM_COMP_PRIORITY_BIT1
[ZRAM_FLAG_SHIFT+8] = ZRAM_COMP_PRIORITY_BIT2
ZRAM_FLAG_SHIFT = PAGE_SHIFT + 1(第 39 行),因此数据大小最大可编码到一页以上,预留了标志位空间。
struct zram {
struct zram_table_entry *table; /* 每个逻辑块一个 entry 的数组 */
struct zs_pool *mem_pool; /* zsmalloc pool */
struct zcomp *comps[ZRAM_MAX_COMPS]; /* 最多 4 个压缩算法实例 */
struct zcomp_params params[ZRAM_MAX_COMPS];
struct gendisk *disk; /* 通用磁盘结构(块设备接口) */
struct rw_semaphore dev_lock; /* 设备级读写锁 */
unsigned long limit_pages; /* 压缩数据最大占用物理页数 */
struct zram_stats stats; /* 原子统计计数器 */
u64 disksize; /* 设备逻辑大小(字节) */
const char *comp_algs[ZRAM_MAX_COMPS]; /* 压缩算法名称数组 */
s8 num_active_comps; /* 当前活跃的压缩算法数量 */
bool claim; /* 设备是否被独占 */
#ifdef CONFIG_ZRAM_WRITEBACK
struct file *backing_dev; /* 后备块设备文件 */
bool wb_limit_enable;
bool compressed_wb; /* 写回时是否保持压缩格式 */
u32 wb_batch_size;
u64 bd_wb_limit;
struct block_device *bdev; /* 后备块设备 */
unsigned long *bitmap; /* 后备设备块的占用位图 */
unsigned long nr_pages; /* 后备设备总块数 */
#endif
};struct zram_stats {
atomic64_t compr_data_size; /* 所有压缩数据总字节数 */
atomic64_t failed_reads;
atomic64_t failed_writes;
atomic64_t notify_free; /* TRIM/discard 通知次数 */
atomic64_t same_pages; /* 全零/全同页面数量 */
atomic64_t huge_pages; /* 当前不可压缩页数 */
atomic64_t huge_pages_since; /* 历史累计不可压缩页数 */
atomic64_t pages_stored; /* 当前存储的总页数 */
atomic_long_t max_used_pages; /* 历史峰值占用物理页数 */
atomic64_t miss_free; /* 释放不存在 slot 的次数 */
#ifdef CONFIG_ZRAM_WRITEBACK
atomic64_t bd_count; /* 后备设备上的页数 */
atomic64_t bd_reads;
atomic64_t bd_writes;
#endif
};zram_add()(第 3053 行)创建并注册一个 zram 块设备:
static int zram_add(void)
{
struct queue_limits lim = {
.logical_block_size = ZRAM_LOGICAL_BLOCK_SIZE, /* 4096 */
.physical_block_size = PAGE_SIZE,
.io_min = PAGE_SIZE,
.io_opt = PAGE_SIZE,
.max_hw_discard_sectors = UINT_MAX,
.features = BLK_FEAT_STABLE_WRITES | BLK_FEAT_SYNCHRONOUS,
};
zram = kzalloc_obj(struct zram);
device_id = idr_alloc(&zram_index_idr, zram, 0, 0, GFP_KERNEL);
/* 创建 gendisk 结构 */
zram->disk = blk_alloc_disk(&lim, NUMA_NO_NODE);
zram->disk->major = zram_major;
zram->disk->fops = &zram_devops;
snprintf(zram->disk->disk_name, 16, "zram%d", device_id);
...
add_disk(zram->disk);
}注意 BLK_FEAT_SYNCHRONOUS 标志表示所有 IO 都是同步完成的(不需要 block layer 的请求调度),这与 zram 的内存操作特性相符。
zram_bio_write(zram, bio)
|
+-- for each bio_vec in bio:
|
v
zram_bvec_write(zram, &bv, index, offset, bio)
|
+-- is_partial_io? --> zram_bvec_write_partial (读-改-写)
|
v
zram_write_page(zram, page, index) [第 2252 行]
|
+-- [1] 检测全同页面(page_same_filled)
| --> write_same_filled_page(): 仅记录元素值,不分配 zsmalloc
|
+-- [2] 压缩
| zstrm = zcomp_stream_get(comps[ZRAM_PRIMARY_COMP])
| ret = zcomp_compress(comps[PRIMARY], zstrm, mem, &comp_len)
|
+-- [3] 压缩结果 >= huge_class_size?
| --> write_incompressible_page():
| zs_malloc(PAGE_SIZE)
| zs_obj_write(... src=原始页 ...)
| set_slot_flag(ZRAM_HUGE)
|
+-- [4] 正常压缩路径
handle = zs_malloc(mem_pool, comp_len, ...)
zs_obj_write(mem_pool, handle, zstrm->buffer, comp_len)
set_slot_handle(zram, index, handle)
set_slot_size(zram, index, comp_len)
全同页面优化(第 2196-2209 行):如果一整页都是相同的值(最典型的是全零页),不分配任何 zsmalloc 空间,仅在 table[index].handle 中存储该值,并设置 ZRAM_SAME 标志。读取时直接用 memset 填充。
zram_bio_read(zram, bio)
|
+-- for each bio_vec:
|
v
zram_bvec_read(zram, &bv, index, offset, bio) [第 2188 行]
|
v
zram_read_page(zram, page, index, bio) [第 2141 行]
|
+-- slot_lock(zram, index)
|
+-- test_slot_flag(ZRAM_WB)?
| 是:get_slot_handle() 得到后备设备 blk_idx
| slot_unlock() 然后 read_from_bdev(blk_idx)
|
+-- 否:read_from_zspool(zram, page, index) [第 2129 行]
|
+-- ZRAM_SAME? --> read_same_filled_page (memset)
+-- ZRAM_HUGE? --> read_incompressible_page
(zs_obj_read_sg + memcpy)
+-- 正常: --> read_compressed_page
(解压缩)
zram 有三种特殊页面处理路径,从写入路径可以总结如下:
输入页面
|
+--- 是全同页(所有字节相同)?
| YES --> 设置 ZRAM_SAME flag,handle 存储元素值
| 读取:memset(page, element, PAGE_SIZE)
|
+--- 压缩后 >= huge_class_size?
| YES --> 设置 ZRAM_HUGE flag,原样存入 zsmalloc
| handle 为 zsmalloc handle
| 读取:zs_obj_read_sg + memcpy
|
+--- 正常压缩
--> handle 为 zsmalloc handle,flags 包含 size
读取:解压缩
CONFIG_ZRAM_WRITEBACK 允许将不常访问(idle)或不可压缩(huge)的页面写回到一个后备块设备:
echo /dev/sdb > /sys/block/zram0/backing_dev # 设置后备设备
echo all > /sys/block/zram0/idle # 标记所有页为 idle
echo idle > /sys/block/zram0/writeback # 将 idle 页写回
写回流程(zram_writeback_slots,第 1048 行):
scan_slots(): 遍历所有 slot,将满足条件的加入 pp_ctl(post-processing control)
|
v
按大小排序(pp_buckets)-- 优先处理大的压缩对象(writeback 收益更大)
|
v
对每个选中的 slot:
+-- zram_reserve_bdev_block() -- 从 bitmap 分配后备设备块
+-- read_from_zspool()/read_from_zspool_raw() -- 读取压缩数据
+-- 构建 bio,提交写请求
+-- 成功后:set_slot_flag(ZRAM_WB),设置 handle 为 blk_idx
释放 zsmalloc 空间(slot_free + zs_free)
写回时若 compressed_wb == true,则将压缩格式的数据直接写入后备设备(read_from_zspool_raw),读回时需要再次解压。
CONFIG_ZRAM_MULTI_COMP 允许最多 4 种压缩算法(ZRAM_MAX_COMPS = 4),支持再压缩(recompress)操作:
echo algo1 algo2 > /sys/block/zram0/comp_algorithm # 设置主/辅算法
echo idle > /sys/block/zram0/recompress # 用辅助算法再压缩 idle 页面
主算法(index 0)用于初始压缩,辅助算法用于后处理再压缩(通常选择压缩比更好但速度较慢的算法,如 zstd)。ZRAM_COMP_PRIORITY_BIT1/BIT2 标志位记录最后使用的压缩算法索引(0-3)。
典型场景:zswap 在前,/dev/zram0 在后
进程内存 zswap zram (/dev/zram0)
+----------+ +----------+ +----------+
| 匿名页 | swap out | 压缩缓存 | writeback | 压缩块设备|
| (4KB/页) |--------------->| (zsmalloc|--------------->| (zsmalloc|
+----------+ | handle) | | handle) |
^ +----------+ +----------+
| | |
| swap in | zswap_load |
| (无磁盘IO) | (命中时) |
+---------------------------+ |
| |
| swap in (zswap 未命中) |
+--------------------------------------------------------+
两者对 zsmalloc 的使用方式相同:
zswap_pool->zs_pool由zs_create_pool("zswapXX")创建zram->mem_pool由zs_create_pool("zramN")创建(disksize_store中调用)- 均使用
zs_malloc分配,zs_obj_write写入,zs_obj_read_sg_begin/end读取,zs_free释放
| 操作 | 集成点 | 函数 |
|---|---|---|
| swap out | mm/page_io.c:275 |
zswap_store(folio) |
| swap in | mm/swap_state.c 的 do_swap_page 路径 |
zswap_load(folio) |
| swap on | mm/swapfile.c:swapon |
zswap_swapon(type, nr_pages) |
| swap off | mm/swapfile.c:swapoff |
zswap_swapoff(type) |
| swap 条目无效 | mm/swapfile.c |
zswap_invalidate(swp) |
代码注释明确定义了锁顺序(第 19-24 行):
page_lock (struct page 的 PG_locked)
|
pool->lock (rwlock_t,保护 zspage 迁移)
|
class->lock (spinlock_t,保护 fullness_list 和 zspage 操作)
|
zspage->zsl (zspage_lock,保护跨页对象访问)
代码注释(第 148-152 行):
zswap_tree.lock (XArray 内部锁,保护 xarray 操作)
|
zswap_pool.lru_lock (list_lru 内部锁,保护 LRU 链表)
注:lru_lock 不需要在持有 tree.lock 的情况下才能获取(writeback 路径会在拿到 lru_lock 后再去 xa_load 验证)。
zram 使用 bit-lock 实现 per-slot 锁(减少内存占用):
// slot_lock (第 94-101 行)
static void slot_lock(struct zram *zram, u32 index)
{
unsigned long *lock = &zram->table[index].__lock;
mutex_acquire(slot_dep_map(zram, index), 0, 0, _RET_IP_);
wait_on_bit_lock(lock, ZRAM_ENTRY_LOCK, TASK_UNINTERRUPTIBLE);
lock_acquired(slot_dep_map(zram, index), _RET_IP_);
}__lock 字段与 attr 共用同一内存(union),通过位操作实现锁。
| 参数 | 路径 | 默认值 | 说明 |
|---|---|---|---|
enabled |
/sys/module/zswap/parameters/enabled |
Y |
全局开关 |
compressor |
/sys/module/zswap/parameters/compressor |
编译时配置 | 压缩算法(如 lz4, zstd) |
max_pool_percent |
/sys/module/zswap/parameters/max_pool_percent |
20 |
zswap 最多占用总内存的百分比 |
accept_threshold_percent |
/sys/module/zswap/parameters/accept_threshold_percent |
90 |
池满后恢复接受新页的阈值(占 max 的百分比) |
shrinker_enabled |
/sys/module/zswap/parameters/shrinker_enabled |
Y |
是否启用动态 shrinker |
max_pool_percent 控制阈值(第 440-443 行):
static unsigned long zswap_max_pages(void)
{
return totalram_pages() * zswap_max_pool_percent / 100;
}| 接口 | 说明 |
|---|---|
/sys/block/zramN/disksize |
设置设备大小(需在初始化前) |
/sys/block/zramN/comp_algorithm |
读写压缩算法 |
/sys/block/zramN/mem_limit |
限制 zsmalloc 可使用的物理内存 |
/sys/block/zramN/mem_used_total |
当前 zsmalloc 占用的物理内存 |
/sys/block/zramN/compr_data_size |
所有压缩数据总大小 |
/sys/block/zramN/backing_dev |
后备块设备路径 |
/sys/block/zramN/writeback |
触发写回操作 |
/sys/block/zramN/idle |
标记 idle 页面 |
/sys/block/zramN/recompress |
触发再压缩(需 MULTI_COMP) |
| 参数 | 位置 | 说明 |
|---|---|---|
CONFIG_ZSMALLOC_CHAIN_SIZE |
Kconfig | 单个 zspage 最大页数(影响 huge class 阈值) |
CONFIG_ZSMALLOC_STAT |
Kconfig | 启用 debugfs 统计 |
zsmalloc 统计通过 debugfs 暴露(/sys/kernel/debug/zsmalloc/<pool_name>/classes),可以看到每个 size class 的对象分布和利用率。
路径:/sys/kernel/debug/zswap/(第 1729-1761 行)
| 文件 | 含义 |
|---|---|
pool_total_size |
zswap 压缩数据总占用字节 |
stored_pages |
当前存储的页面总数 |
stored_incompressible_pages |
不可压缩页面数量(原样存储) |
pool_limit_hit |
触发 pool 上限的次数 |
reject_reclaim_fail |
淘汰失败导致拒绝存储的次数 |
reject_alloc_fail |
zsmalloc 分配失败次数 |
reject_kmemcache_fail |
entry 元数据分配失败次数 |
reject_compress_fail |
压缩算法失败次数 |
reject_compress_poor |
压缩效果差(输出 >= 输入)导致拒绝次数 |
decompress_fail |
解压失败次数 |
written_back_pages |
shrinker 写回到 swap 设备的页面数 |
通过 /proc/vmstat 可见(第 1532-1534 行、第 1622 行):
ZSWPOUT # zswap 写入(swap out 被 zswap 拦截)
ZSWPIN # zswap 读取命中(无磁盘 IO)
ZSWPWB # zswap writeback(淘汰到 swap 设备)
cat /sys/block/zram0/mm_stat
# 输出格式:
# orig_data_size compr_data_size mem_used_total mem_limit mem_used_max
# same_pages compact_pages huge_pages
cat /sys/block/zram0/io_stat
# 输出:failed_reads failed_writes notify_free invalid_io组合使用时,zram 作为 swap 设备,zswap 作为 zram 的前端缓存:
zswap -> zram (swap 设备)
此时 zswap 压缩 + zram 再压缩会有双重压缩开销,且 zram 已经在 RAM 中,zswap 的优势(减少磁盘 IO)不再明显。一般建议二选一,或者在内存极度受限时才组合使用。
当 entry->length == PAGE_SIZE(zswap)或 ZRAM_HUGE 标志(zram)时,页面原样存储于 zsmalloc,占用一个物理页的空间(外加少量元数据)。在压缩比差的工作负载下,这会导致实际占用内存高于期望。
zswap 的处理策略:若 mem_cgroup_zswap_writeback_enabled 为 false(memcg 禁用 writeback),则直接拒绝不可压缩页面(第 895-900 行),避免无效占用内存。
zs_malloc 在 zswap 路径中使用 GFP_NOWAIT | __GFP_NORETRY(第 907 行),意味着分配失败时不会重试也不会触发内存回收。这是为了防止在 swap out 路径上触发递归回收造成死锁。失败时 zswap_store 返回 false,系统回退到直接写 swap 设备。
zswap_load 明确检查并拒绝 large folio(第 1606-1609 行):
if (WARN_ON_ONCE(folio_test_large(folio))) {
folio_unlock(folio);
return -EINVAL;
}zswap 当前不支持 large folio(多页 THP 的 swap),因为 zswap 是以单页为粒度存储的(每个 zswap_entry 对应一个 4K 页)。这是 zswap 目前的已知限制。
zspage_lock 使用了一种非标准的读写锁设计(第 255-352 行):
- 写锁:总是原子的,使用 trylock(不能 sleep)
- 读锁:可以 sleep(因为只有原子写者)
这是因为 zspage 读锁可能在进程上下文中长期持有(在读/写对象期间),而写锁只用于内存规整(compaction),规整操作可以重试,因此接受 trylock 失败。
每个 zswap_pool 在 cpuhp_state_add_instance 中注册了 CPU hotplug 回调(第 275 行):
- CPU online:
zswap_cpu_comp_prepare分配该 CPU 的crypto_acomp_ctx - CPU offline:
zswap_cpu_comp_dead释放该 CPU 的上下文
这保证了即使在 CPU 热插拔期间也不会访问已释放的压缩上下文(第 836-845 行的自旋等待逻辑处理了极端竞争情况)。
理解 zswap 的演进历史有助于理解当前架构的设计决策。
在 Linux 5.x 及更早版本中,zswap 通过 mm/zpool.c 定义的 struct zpool_ops 抽象层对接不同的内存池后端。这一设计允许运行时选择后端:
旧版 zswap 架构(已废弃):
zswap_pool
|
+-- struct zpool *zpool <-- 抽象接口
|
+-- zbud_zpool_ops (每页存 2 个压缩对象)
+-- z3fold_zpool_ops (每页存最多 3 个压缩对象)
+-- zsmalloc_zpool_ops (可变大小,无固定上限)
zbud(mm/zbud.c)是最简单的实现:每个物理页严格存储 2 个压缩对象,页内分成左右两半。优点是实现简单,缺点是内存利用率最多 50%(极端情况下每半都只有 1 字节有效数据)。
z3fold(mm/z3fold.c)改进了这一点,允许每页存放最多 3 个对象,利用率可达约 67%,但实现复杂度大幅上升,且在某些边界情况下仍有显著的内部碎片。
从 Linux 6.x 开始(具体参见 commit 8b0d88a6d68f),zswap 彻底移除了 zpool 抽象层,直接使用 zsmalloc。这一变化带来以下优势:
新版 zswap 架构(当前):
zswap_pool
|
+-- struct zs_pool *zs_pool <-- 直接使用 zsmalloc
- 内存利用率更高:zsmalloc 对每种大小的对象有专门的 size class,理论利用率接近 100%
- 无锁竞争改善:消除了 zpool 接口层的额外锁
- 代码简化:移除了 zbud、z3fold 的维护负担
- 更好的 NUMA 支持:
zs_malloc接受nid参数,支持 NUMA 本地分配
虽然 zbud 已从主线移除(相关代码保留在历史版本中),其设计思路值得了解:
zbud 页面布局(PAGE_SIZE = 4096 字节):
+-------+------------------+--+---------+--------+
| zbud | left object | | right | zbud |
| hdr | (压缩数据,可变长)|空| object | hdr |
| (8B) | |白| (从右起)| (8B) |
+-------+------------------+--+---------+--------+
^ ^
页面起始 页面结束
- 左对象从页面前端向后增长
- 右对象从页面后端向前增长
- 中间是空洞(不可利用的浪费空间)
- 每页最多存 2 个对象,不足 2 个时直接从 buddy 系统申请新页
zbud 的内存浪费来自中间空洞。当左对象 800 字节、右对象 200 字节时,4096 - 800 - 200 - 16(头部) = 3080 字节被浪费。
z3fold 通过将每页分成三个插槽来改进利用率,但引入了复杂的 "folding" 和 "unfolding" 操作(将两个半满页合并为一个更满的页),这在压力下容易死锁,也是它最终被废弃的原因。
zsmalloc 高利用率的根本原因是其按对象大小分组的策略。对于 200 字节的压缩对象,zsmalloc 选择 200 字节的 size class,用 2 个物理页构建一个 zspage,存放 (4096*2) / (200+8) = 39 个对象,利用率约 39*200/(4096*2) ≈ 95%。
不同内存池的利用率对比(以 200 字节对象为例):
zbud: ≈ (200+200) / 4096 ≈ 10% (浪费严重)
z3fold: ≈ (200*3) / 4096 ≈ 15% (有改善但仍低)
zsmalloc: ≈ (200*39) / 8192 ≈ 95% (近似最优)
注:zbud/z3fold 对于接近 2048 字节的大对象利用率会接近 50%/67%,
而 zsmalloc 在所有大小下均能保持高利用率。
源码文件:drivers/block/zram/zcomp.c、drivers/block/zram/backend_*.c
zram 通过 struct zcomp_ops(zcomp.h,第 57-70 行)定义了压缩算法的统一接口:
struct zcomp_ops {
int (*compress)(struct zcomp_params *params, struct zcomp_ctx *ctx,
struct zcomp_req *req);
int (*decompress)(struct zcomp_params *params, struct zcomp_ctx *ctx,
struct zcomp_req *req);
int (*create_ctx)(struct zcomp_params *params, struct zcomp_ctx *ctx);
void (*destroy_ctx)(struct zcomp_ctx *ctx);
int (*setup_params)(struct zcomp_params *params);
void (*release_params)(struct zcomp_params *params);
const char *name;
};这一设计将算法参数(zcomp_params,全局共享)与运行时上下文(zcomp_ctx,per-CPU)分离,使得压缩字典等共享数据可以在所有 CPU 之间复用,而压缩工作内存(context)保持独立。
全局后端注册表(zcomp.c,第 23-44 行):
static const struct zcomp_ops *backends[] = {
#if IS_ENABLED(CONFIG_ZRAM_BACKEND_LZO)
&backend_lzorle,
&backend_lzo,
#endif
#if IS_ENABLED(CONFIG_ZRAM_BACKEND_LZ4)
&backend_lz4,
#endif
#if IS_ENABLED(CONFIG_ZRAM_BACKEND_LZ4HC)
&backend_lz4hc,
#endif
#if IS_ENABLED(CONFIG_ZRAM_BACKEND_ZSTD)
&backend_zstd,
#endif
#if IS_ENABLED(CONFIG_ZRAM_BACKEND_DEFLATE)
&backend_deflate,
#endif
#if IS_ENABLED(CONFIG_ZRAM_BACKEND_842)
&backend_842,
#endif
NULL
};BUILD_BUG_ON(ARRAY_SIZE(backends) <= 1) 确保至少有一个后端被编译(第 239 行)。
源码文件:drivers/block/zram/backend_lzo.c(60 行)
lzo 是 zram 的默认压缩算法(当未选择其他后端时),由 CONFIG_ZRAM_BACKEND_FORCE_LZO 强制启用(Kconfig 第 47-51 行)。
lzo 后端结构极为简洁。运行时上下文(zcomp_ctx)只包含一块 LZO1X_MEM_COMPRESS 大小的工作内存:
// backend_lzo.c:18-23
static int lzo_create(struct zcomp_params *params, struct zcomp_ctx *ctx)
{
ctx->context = kzalloc(LZO1X_MEM_COMPRESS, GFP_KERNEL);
if (!ctx->context)
return -ENOMEM;
return 0;
}压缩调用(第 31-38 行)直接调用 lzo1x_1_compress,解压调用 lzo1x_decompress_safe,接口几乎是一行代码。
lzo-rle 是 lzo 的改进变体,针对含有大量连续相同字节的数据(run-length encoding 优化),对内存页中的零值区域压缩效果更好。在 Android 等嵌入式系统中常见。默认 zram 使用 lzo-rle(Kconfig 第 65-68 行)。
lzo 特征总结:
- 压缩速度:极快(约 250-400 MB/s)
- 解压速度:极快(约 600-800 MB/s)
- 压缩比:中等(约 2:1 到 2.5:1,取决于数据类型)
- 内存开销:极小(~8KB 工作内存)
- 适用场景:低延迟要求,CPU 资源紧张的嵌入式设备
源码文件:drivers/block/zram/backend_lz4.c(128 行)
lz4 是目前最流行的 zram 算法之一,以极高的解压速度著称。其运行时上下文(lz4_ctx,第 8-13 行)结构如下:
struct lz4_ctx {
void *mem; /* LZ4_MEM_COMPRESS 大小的工作内存 */
LZ4_streamDecode_t *dstrm; /* 字典解压流(仅在使用字典时分配) */
LZ4_stream_t *cstrm; /* 字典压缩流(仅在使用字典时分配) */
};当 params->dict_sz == 0(无字典,默认情况)时,使用 LZ4_MEM_COMPRESS 大小的简单工作内存;当指定了压缩字典时,分配 LZ4_stream_t 和 LZ4_streamDecode_t(第 49-61 行)。
压缩调用(第 70-93 行):
static int lz4_compress(struct zcomp_params *params, struct zcomp_ctx *ctx,
struct zcomp_req *req)
{
if (!zctx->cstrm) {
// 无字典:LZ4_compress_fast
ret = LZ4_compress_fast(req->src, req->dst, req->src_len,
req->dst_len, params->level, zctx->mem);
} else {
// 有字典:需先 LZ4_loadDict 重置流状态,再压缩
ret = LZ4_loadDict(zctx->cstrm, params->dict, params->dict_sz);
ret = LZ4_compress_fast_continue(zctx->cstrm, req->src, req->dst,
req->src_len, req->dst_len,
params->level);
}
req->dst_len = ret;
return 0;
}params->level 对应 lz4 的 acceleration 参数:值越大压缩越快但压缩比越低(第 21-24 行,默认为 LZ4_ACCELERATION_DEFAULT)。
lz4hc 是 lz4 的高压缩比变体,通过更深度的哈希链搜索(HC = High Compression)来提升压缩比,代价是压缩速度下降约 3-5 倍。解压速度与 lz4 相同,因为解压算法无需变化。
lz4 vs lz4hc 对比:
lz4 lz4hc
压缩速度: ~400 MB/s ~80-120 MB/s
解压速度: ~2000 MB/s ~2000 MB/s
压缩比: ~2.5:1 ~3.0:1
内存开销: ~128KB ~256KB
适用场景: swap 热路径 后台再压缩
源码文件:drivers/block/zram/backend_zstd.c(218 行)
zstd 是当前最先进的通用压缩算法,也是 zram 支持的压缩比最高的算法。其实现相对复杂,需要分离的压缩/解压上下文。
运行时上下文(第 10-15 行):
struct zstd_ctx {
zstd_cctx *cctx; /* 压缩上下文 */
zstd_dctx *dctx; /* 解压上下文 */
void *cctx_mem; /* 无字典时:cctx 使用的预分配内存 */
void *dctx_mem; /* 无字典时:dctx 使用的预分配内存 */
};参数结构(第 17-22 行)包含字典相关数据:
struct zstd_params {
zstd_custom_mem custom_mem; /* 字典模式下的自定义内存分配器 */
zstd_cdict *cdict; /* 预处理的压缩字典 */
zstd_ddict *ddict; /* 预处理的解压字典 */
zstd_parameters cprm; /* 压缩参数(级别等) */
};字典支持的实现细节(backend_zstd.c 第 51-90 行):
当 params->dict_sz > 0 时,zstd_setup_params 会预编译字典(zstd_create_cdict_byreference / zstd_create_ddict_byreference)。字典数据由 algorithm_params_store 从文件系统读取(zram_drv.c 第 1700 行:kernel_read_file_from_path)。
字典模式下的内存分配使用 zstd_custom_alloc(第 28-31 行),其内部调用 kvzalloc,允许在回退到 vmalloc 之前先尝试 kmalloc:
static void *zstd_custom_alloc(void *opaque, size_t size)
{
return kvzalloc(size, GFP_NOIO | __GFP_NOWARN);
}创建上下文时的差异(第 119-167 行):
无字典时,zstd_create 分配固定大小的内存块并用 zstd_init_cctx/dctx 在其中初始化上下文;有字典时,使用 zstd_create_cctx_advanced(调用 zstd 内部分配器)。这种差异意味着字典模式每个 CPU 的内存开销更高但更灵活。
zstd 特征总结:
- 压缩速度:中等(约 200-400 MB/s,取决于级别)
- 解压速度:快(约 800-1000 MB/s)
- 压缩比:高(约 3:1 到 4:1,取决于数据)
- 内存开销:中等(约 1-4MB 工作内存,取决于级别)
- 字典支持:是(可以为特定工作负载训练专用字典)
- 适用场景:后台再压缩,对压缩比要求高的场景
deflate(backend_deflate.c):基于 zlib 的经典 DEFLATE 算法,支持配置 winbits(窗口大小)参数。在 zcomp_params 中有专属的 struct deflate_params(zcomp.h 第 10-12 行)。deflate 的优势是标准性(与 gzip 兼容),但在速度和压缩比上均劣于 lz4 和 zstd。
842(backend_842.c):IBM Power 处理器的硬件辅助压缩算法。当运行在 Power 平台时,内核可以利用 POWER NX(Nest Accelerator)硬件单元进行硬件加速压缩/解压,显著降低 CPU 开销。在非 Power 平台上退化为软件实现,性能一般。
所有支持算法的综合对比:
算法 压缩速度 解压速度 压缩比 内存开销 字典支持
lzo-rle 极快 极快 中 极小 否
lzo 极快 极快 中 极小 否
lz4 极快 极快 中高 小 是
lz4hc 中 极快 高 中 是
zstd 中 快 很高 中 是
deflate 慢 中 高 中 否
842 快(hw) 快(hw) 中 小 否
选择压缩算法时需要权衡以下因素:
场景一:移动设备 / 嵌入式系统
- 推荐:
lzo-rle(默认)或lz4 - 理由:CPU 资源紧张,优先速度;内存工作集多含零值页,lzo-rle 效果好
场景二:桌面 / 服务器(充裕 CPU)
- 主算法:
lz4(快速初始压缩) - 辅助算法:
zstd(空闲时再压缩,提升压缩比) - 需启用
CONFIG_ZRAM_MULTI_COMP
场景三:高压缩比需求(内存极度紧缺)
- 推荐:
zstd(高压缩比),可配合字典进一步提升 - 代价:CPU 开销较高,需要充分测试
算法切换命令:
# 切换到 lz4(设备初始化前)
echo lz4 > /sys/block/zram0/comp_algorithm
# 设置压缩级别(通过 algorithm_params)
echo "algo=lz4 level=8" > /sys/block/zram0/algorithm_params
# MULTI_COMP 配置:主算法 lz4,辅算法 zstd
echo "algo=lz4" > /sys/block/zram0/comp_algorithm
echo "algo=zstd priority=1" > /sys/block/zram0/recomp_algorithmzram 的并发性能关键在于 per-CPU 压缩流(stream) 机制。每个 CPU 独立拥有一套压缩上下文,包括:
- 压缩工作内存(
zcomp_ctx.context,由算法后端管理) - 压缩输出缓冲区(
zcomp_strm.buffer,2 个物理页) - 用于跨页对象的本地副本(
zcomp_strm.local_copy,1 个物理页) - 保护此流的互斥锁(
zcomp_strm.lock)
zcomp (每个 zram 设备有最多 4 个 zcomp,对应最多 4 种算法)
|
+-- stream (per-CPU 数组,每个 CPU 一个 zcomp_strm)
|
+-- CPU 0: zcomp_strm { lock, buffer(2 pages), local_copy(1 page), ctx }
+-- CPU 1: zcomp_strm { lock, buffer(2 pages), local_copy(1 page), ctx }
+-- CPU 2: zcomp_strm { lock, buffer(2 pages), local_copy(1 page), ctx }
+-- ...
缓冲区分配(zcomp.c 第 54-73 行):
static int zcomp_strm_init(struct zcomp *comp, struct zcomp_strm *zstrm)
{
ret = comp->ops->create_ctx(comp->params, &zstrm->ctx);
zstrm->local_copy = vzalloc(PAGE_SIZE);
/*
* allocate 2 pages: 1 for compressed data, plus 1 extra for the
* case when compressed size is larger than the original one
*/
zstrm->buffer = vzalloc(2 * PAGE_SIZE);
...
}分配 2 * PAGE_SIZE 的缓冲区是为了处理"压缩失败"的情况:当压缩输出大于输入时,需要额外空间存放溢出数据,而不是直接写坏内存。
zcomp_stream_get(zcomp.c 第 110-130 行)的实现有一个微妙的竞争处理:
struct zcomp_strm *zcomp_stream_get(struct zcomp *comp)
{
for (;;) {
struct zcomp_strm *zstrm = raw_cpu_ptr(comp->stream);
/*
* stream is returned with ->mutex locked which prevents
* cpu_dead() from releasing this stream under us, however
* there is still a race window between raw_cpu_ptr() and
* mutex_lock(), during which we could have been migrated
* from a CPU that has already destroyed its stream. If
* so then unlock and re-try on the current CPU.
*/
mutex_lock(&zstrm->lock);
if (likely(zstrm->buffer))
return zstrm;
mutex_unlock(&zstrm->lock);
}
}这里使用 raw_cpu_ptr 而非 get_cpu_ptr(后者会禁止抢占)的原因:在 mutex_lock 能睡眠的情况下,不能持有禁止抢占的引用。竞争处理策略是:
- 用
raw_cpu_ptr获取当前 CPU 的流指针(此时可能被迁移) - 尝试对该流加互斥锁(可能睡眠,期间可能发生 CPU 迁移)
- 加锁后检查
buffer是否为 NULL(NULL 表示该 CPU 的流已被cpu_dead释放) - 如果 buffer 为 NULL,解锁并重试(此时已经在新 CPU 上)
对应的释放函数极为简单(第 132-135 行):
void zcomp_stream_put(struct zcomp_strm *zstrm)
{
mutex_unlock(&zstrm->lock);
}zcomp 通过 CPU hotplug 框架管理每 CPU 流(第 169-190 行):
// CPU online 回调
int zcomp_cpu_up_prepare(unsigned int cpu, struct hlist_node *node)
{
struct zcomp *comp = hlist_entry(node, struct zcomp, node);
struct zcomp_strm *zstrm = per_cpu_ptr(comp->stream, cpu);
return zcomp_strm_init(comp, zstrm);
}
// CPU offline 回调
int zcomp_cpu_dead(unsigned int cpu, struct hlist_node *node)
{
struct zcomp *comp = hlist_entry(node, struct zcomp, node);
struct zcomp_strm *zstrm = per_cpu_ptr(comp->stream, cpu);
mutex_lock(&zstrm->lock);
zcomp_strm_free(comp, zstrm); /* 释放 buffer 和 local_copy,设为 NULL */
mutex_unlock(&zstrm->lock);
return 0;
}cpu_dead 时持有 mutex,确保没有正在使用这个流的压缩操作。zcomp_strm_free 将 buffer 设为 NULL(第 51 行),这正是 zcomp_stream_get 检测的条件。
CPU hotplug 注册(zcomp_init,第 205-210 行):
ret = cpuhp_state_add_instance(CPUHP_ZCOMP_PREPARE, &comp->node);注意:CPUHP_ZCOMP_PREPARE 是在 zcomp_init 中静态注册的 hotplug 状态,与 zswap 使用的 CPUHP_MM_ZSWP_POOL_PREPARE 是不同的状态位。
zcomp_strm 内存布局:
buffer (vzalloc, 2 * PAGE_SIZE):
+------------------+------------------+
| 压缩输出区 | 溢出保护区 |
| (最多 PAGE_SIZE) | (防止越界写) |
+------------------+------------------+
0 4096 8192
local_copy (vzalloc, PAGE_SIZE):
+------------------+
| zsmalloc 跨页对象 |
| 的本地临时副本 |
+------------------+
0 4096
local_copy 用于 zcomp.c 中的 zs_obj_read_begin 接口(注释见 zram_drv.c 第 2110-2116 行):当 zsmalloc 中的对象跨越两个物理页时,需要先将数据复制到连续的 local_copy 缓冲区再进行解压缩。
CONFIG_ZRAM_MULTI_COMP 引入了再压缩功能,允许使用优先级更高(通常压缩比更高)的算法重新压缩已有数据。
zram 定义了最多 4 个压缩优先级(zram_drv.h 第 98-106 行):
#ifdef CONFIG_ZRAM_MULTI_COMP
#define ZRAM_PRIMARY_COMP 0U /* 主算法:初始写入时使用 */
#define ZRAM_SECONDARY_COMP 1U /* 次要算法:再压缩的起始优先级 */
#define ZRAM_MAX_COMPS 4U /* 最大算法数量 */
#else
#define ZRAM_PRIMARY_COMP 0U
#define ZRAM_SECONDARY_COMP 0U
#define ZRAM_MAX_COMPS 1U
#endif每个 slot 的 ZRAM_COMP_PRIORITY_BIT1/BIT2 标志位(共 2 位)记录该 slot 当前使用的压缩算法优先级索引(0-3),用于再压缩时的进度追踪。
核心函数 recompress_slot(第 2395-2530 行)的执行逻辑如下:
recompress_slot(zram, index, page, num_recomp_pages, threshold, prio, prio_max)
|
+-- 1. 获取当前 slot 的压缩长度(comp_len_old)
| 若 comp_len_old < threshold,直接返回(无需再压缩)
|
+-- 2. read_from_zspool(zram, page, index)
| 将当前数据解压到临时页面
|
+-- 3. 清除 ZRAM_IDLE 标志(我们访问了这个 slot)
|
+-- 4. zs_lookup_class_index(mem_pool, comp_len_old)
| 获取旧对象所在的 size class 索引
|
+-- 5. 调整起始优先级:
| prio = max(prio, get_slot_comp_priority(index) + 1)
| (不重复尝试已经用过的更低优先级算法)
|
+-- 6. 按优先级迭代尝试再压缩:
| for prio in [prio, prio_max):
| zstrm = zcomp_stream_get(comps[prio])
| zcomp_compress(comps[prio], zstrm, page, &comp_len_new)
| class_index_new = zs_lookup_class_index(mem_pool, comp_len_new)
| if class_index_new >= class_index_old: continue (无收益)
| if threshold && comp_len_new >= threshold: continue (未达阈值)
| break (再压缩有收益)
|
+-- 7. 若再压缩有收益:
| handle_new = zs_malloc(mem_pool, comp_len_new, ...)
| zs_obj_write(mem_pool, handle_new, zstrm->buffer, comp_len_new)
| slot_free(zram, index) (释放旧 handle)
| set_slot_handle(index, handle_new)
| set_slot_comp_priority(index, prio)
| clear_slot_flag(index, ZRAM_HUGE) (如果之前是不可压缩页)
|
+-- 8. 递减 num_recomp_pages(计入本次尝试,不论是否成功)
注意第 4 步:使用 zs_lookup_class_index 而非直接比较字节数来判断是否有收益。这是因为 zsmalloc 会将相邻大小的 class 合并,只有当新压缩大小落入不同(更小)的 size class 时,才能实际节省内存。
为了避免在扫描所有 slot 时长期持有 slot 锁,zram 引入了 zram_pp_ctl(post-processing control)和 zram_pp_slot 结构:
scan_slots_for_recompress / scan_slots_for_writeback
|
+-- 遍历所有 slot(短暂持锁),满足条件者加入 pp_ctl
|
v
pp_ctl (post-processing control)
|
+-- pp_slots[] 数组(按压缩大小分桶,优先处理大的)
|
v
select_pp_slot() -- 按优先级顺序取出 slot
|
v
对选出的 slot 执行 recompress_slot() 或 writeback
(持 slot 锁,但时间短)
这种"先扫描注册,后执行"的两阶段设计避免了在整个 writeback/recompress 过程中持有大粒度锁。
zsmalloc 的 handle 是一个双重间接指针,完整的寻址链路如下:
用户持有的 handle(unsigned long)
|
| 解引用 (handle_to_obj)
v
obj 值(unsigned long,存于 handle_cachep 分配的 8 字节内存中)
|
| obj_to_location 解码
v
(PFN, obj_idx) 元组
|
| pfn_zpdesc + 计算偏移
v
zspage 中的物理内存位置
[zpdesc0/page0 + offset](单页)
或
[zpdesc0/page0 + offset0] + [zpdesc1/page1 + 0](跨页)
这种二级间接寻址的核心价值在于 compaction 时的透明性:当 compaction 将对象从 zspage A 迁移到 zspage B 时,只需将 handle 指向的 obj 值从指向 A 更新为指向 B,所有持有 handle 的调用方(zswap_entry.handle、zram_table_entry.handle)不需要任何修改。
obj 值(mm/zsmalloc.c 第 77-92 行)的位域布局:
obj 值(64 位系统,假设 MAX_POSSIBLE_PHYSMEM_BITS = 52):
位 [63 : OBJ_INDEX_BITS] = PFN (物理帧号,约 40 位)
位 [OBJ_INDEX_BITS-1 : 1] = 对象在该物理页中的索引
位 [0] = OBJ_ALLOCATED_TAG (1 = 已分配)
OBJ_INDEX_BITS = BITS_PER_LONG - _PFN_BITS
= 64 - (MAX_POSSIBLE_PHYSMEM_BITS - PAGE_SHIFT)
= 64 - (52 - 12)
= 24 (对象索引最多 24 位,即每页最多 2^23 个对象)
OBJ_ALLOCATED_TAG(第 86 行)用于区分已分配对象的 handle 区(存储 obj 值)和空闲对象的 link_free 区(存储下一个空闲对象的索引)。
当压缩对象跨越两个物理页时,zsmalloc 需要对两个页面分别进行 kmap 操作:
跨页对象读写策略:
情形一:单页内(off + size <= PAGE_SIZE)
kmap_local_zpdesc(zpdesc)
操作 [dst/src + off, dst/src + off + size)
kunmap_local(addr)
情形二:跨页(off + size > PAGE_SIZE)
第一段:在 zpdesc0 中,[off, PAGE_SIZE) 共 sizes[0] = PAGE_SIZE - off 字节
第二段:在 zpdesc1 中,[0, sizes[1]) 共 sizes[1] = size - sizes[0] 字节
在 x86_64 等支持直接映射的架构上,kmap_local_zpdesc 可以直接返回线性地址而无需建立映射(CONFIG_KMAP_LOCAL)。在 32 位系统或使用 HIGHMEM 的情况下,需要调用 kmap_atomic 建立临时映射。
zram 在某些路径下使用 zs_obj_read_begin(而非 scatter-gather 接口 zs_obj_read_sg_begin),这在读取原始压缩数据时更为简单(zram_drv.c 第 2115-2116 行):
src = zs_obj_read_begin(zram->mem_pool, handle, size, zstrm->local_copy);
memcpy_to_page(page, 0, src, size);
zs_obj_read_end(zram->mem_pool, handle, size, src);当对象在单页内时,src 直接指向 zsmalloc 的内存(通过 kmap 获得);当对象跨页时,src 指向 zstrm->local_copy(预分配的 1 页缓冲区,在 zs_obj_read_begin 内部已将两段数据拷贝到 local_copy 中)。这种设计使调用方无需关心是否跨页。
zram 的 writeback 操作通过 zram_wb_ctl(writeback control)和 zram_wb_req(writeback request)结构协调(zram_drv.c 中定义):
zram_wb_ctl(写回控制器)
|
+-- idle_reqs: 空闲的 bio 请求池(可复用)
+-- done_reqs: 已完成但待处理的 bio 请求列表
+-- done_lock: 保护 done_reqs 的自旋锁
+-- done_wait: 等待完成的 waitqueue
+-- num_inflight: 当前飞行中的 bio 数量
zram_wb_req(单次写回请求)
|
+-- bio: 内嵌的 bio 结构(提交到后备设备)
+-- blk_idx: 在后备设备位图中分配的块索引
+-- pps: 指向对应的 post-processing slot
+-- entry: 链表节点(idle_reqs 或 done_reqs 中)
zram writeback 采用异步 bio + 完成轮询的模式(zram_drv.c 第 977-1035 行):
zram_writeback_slots 主循环:
while (有待写回的 slot):
|
+-- select_pp_slot(ctl) -- 取下一个目标 slot
|
+-- zram_select_idle_req(wb_ctl) -- 取一个空闲的 bio 请求
| 若无空闲请求,先等待已完成的请求
|
+-- zram_reserve_bdev_block(zram, &blk_idx) -- 从位图分配块
|
+-- 读取数据(read_from_zspool 或 read_from_zspool_raw)
|
+-- bio_init(&req->bio, bdev, vvec, 1, REQ_OP_WRITE | REQ_SYNC)
+-- bio->bi_end_io = zram_writeback_endio
|
+-- zram_submit_wb_request(zram, wb_ctl, req)
| -- 提交 bio
| -- atomic_inc(&wb_ctl->num_inflight)
|
+-- 处理已完成请求(zram_complete_done_reqs)
完成回调 zram_writeback_endio(第 977-988 行)将完成的请求加入 done_reqs 并唤醒等待的线程,而不是直接在中断上下文中做繁重的状态更新。实际的状态更新(更新 slot 标志、释放 zsmalloc 空间)在 zram_writeback_complete(第 918-974 行)中完成,在进程上下文中执行。
当 zram->compressed_wb == true 时(通过 /sys/block/zramN/wb_limit_enable 等接口控制),writeback 直接将压缩格式的数据写入后备设备:
compressed_wb = false(默认):
zsmalloc(压缩数据)--> 解压 --> 原始页面 --> 写入后备设备
读回时:后备设备 --> 原始页面 --> 压缩 --> zsmalloc
compressed_wb = true:
zsmalloc(压缩数据)--> 直接写入后备设备(保持压缩格式)
读回时:后备设备 --> 压缩数据 --> 解压 --> 原始页面
compressed_wb 模式的优势:写回时节省解压缩 + 再次压缩的开销;后备设备存储效率更高(压缩数据更小)。
但需要在 slot 中额外保存 size(压缩后大小)和 comp_priority(使用的压缩算法),以便读回时能正确解压(zram_writeback_complete 第 949-967 行保存这些信息)。
CONFIG_ZRAM_TRACK_ENTRY_ACTIME 在每个 zram_table_entry 中增加 u32 ac_time 字段(zram_drv.h 第 72-74 行),记录最近一次访问的时间(boot 秒数,精度约为 1 秒)。
idle 标记流程:
# 标记所有页为 idle(设置 ZRAM_IDLE 标志)
echo all > /sys/block/zram0/idle
# 或使用时间过滤(需要 ZRAM_TRACK_ENTRY_ACTIME):
# 标记超过 3600 秒未访问的页为 idle
echo 3600 > /sys/block/zram0/idle读操作时,zram_bio_read 会清除对应 slot 的 ZRAM_IDLE 标志(zram_drv.c 第 2749 行):
slot_lock(zram, index);
zram_accessed(zram, index); /* 清除 ZRAM_IDLE,更新 ac_time */
slot_unlock(zram, index);通过定期执行"标记 idle -> 等待一段时间 -> writeback idle 页"的循环,可以实现基于访问频率的 zram 内存压缩管理。
zswap 使用内核的 struct list_lru(mm/list_lru.c)实现多维度的 LRU 管理。list_lru 支持 per-NUMA-node 和 per-memcg 的独立链表,是内核中用于 shrinker 的标准 LRU 数据结构。
zswap_list_lru 的初始化(zswap_setup,第 1798 行):
list_lru_init_memcg(&zswap_list_lru, zswap_shrinker);list_lru_init_memcg 会为每个 NUMA 节点和每个 memcg 分配独立的 list_lru_one 结构,每个结构包含一个独立的 struct list_head 和一个 spinlock_t。
zswap_list_lru 内部结构:
list_lru
|
+-- node[0] (NUMA node 0)
| +-- lru (全局链表,按 memcg 分区)
| +-- lru_one for memcg_A
| +-- lru_one for memcg_B
| +-- ...
|
+-- node[1] (NUMA node 1)
+-- ...
当 shrinker 扫描时,通过 list_lru_shrink_walk_irq(第 1113 行)遍历指定 node+memcg 组合的链表,调用 shrink_memcg_cb 回调处理每个条目。
zswap shrinker 的触发有两种路径:
路径一:内核内存压力触发(mm/vmscan.c)
当系统内存压力大时,shrink_slab 会遍历所有注册的 shrinker 并调用:
mm/vmscan.c: shrink_slab()
|
+-- 调用每个注册的 shrinker 的 count_objects() 和 scan_objects()
|
v
zswap_shrinker_count() -- 计算可回收条目数(考虑压缩比调整)
|
v
zswap_shrinker_scan() -- 实际执行回收(调用 shrink_memcg_cb)
路径二:zswap 写入时主动触发
当 zswap_store 发现当前 memcg 的 zswap 使用量超限时(obj_cgroup_may_zswap 返回 false),主动调用 shrink_memcg(第 1502 行):
// zswap.c 第 1500-1510 行(简化)
if (!obj_cgroup_may_zswap(objcg)) {
shrink_memcg(mem_cgroup_from_obj(objcg));
/* 再次检查,若仍超限则拒绝 */
if (!obj_cgroup_may_zswap(objcg))
goto reject;
}zswap 还有一个后台工作队列机制用于全局内存压力下的异步 writeback(第 1312 行):
static void shrink_worker(struct work_struct *w)
{
struct lruvec *lruvec;
int nid, shrunk = 0;
for_each_node_state(nid, N_NORMAL_MEMORY) {
lruvec = mem_cgroup_lruvec(NULL, NODE_DATA(nid));
shrunk += shrink_lruvec(lruvec, zswap_shrinker, &sc);
}
if (!shrunk)
/* 若无法收缩,可能 swap 设备也满了,暂时关闭 zswap */
zswap_enabled = false;
schedule_delayed_work(&zswap_shrink_work, HZ * 5); /* 5 秒后再检查 */
}shrink_worker 在 shrink_wq(WQ_UNBOUND | WQ_MEM_RECLAIM)工作队列上执行,WQ_MEM_RECLAIM 确保即使在内存压力下也能分配执行线程。
zswap writeback 路径的并发安全是其最复杂的部分,涉及三个并发场景:
场景一:writeback 与 zswap_load 并发
时序 1(安全):
T1(writeback): xa_erase(tree, offset) --> 删除 xarray 条目
T2(zswap_load): xa_load(tree, offset) --> 返回 NULL,走磁盘 IO 路径
时序 2(安全):
T2(zswap_load): xa_load(tree, offset) --> 找到 entry
T1(writeback): 正在 decompress...
T2: zswap_decompress(entry, folio) --> 并发解压同一 entry
问题:entry 可能被 T1 在 writeback 后释放
解决:writeback 路径在 xa_erase 前先检验 entry 仍在 xarray 中(第 1024 行的 xa_cmpxchg 验证)
场景二:writeback 与 zswap_store 并发
新的 zswap_store_page 若遇到同一 swap offset 已有条目,会先将旧条目删除(xa_erase + zswap_entry_free),然后插入新条目。writeback 在执行前会重新验证 entry 仍在 xarray 中(通过 xa_load 检查,第 1020-1030 行)。
场景三:writeback 与 memcg 下线并发
当 memcg 下线时,zswap_memcg_offline_cleanup(第 682 行)推进 LRU 游标,确保下线的 memcg 不再参与 shrinker 扫描,避免访问已释放的 memcg 对象。
# 启用 zswap(大多数发行版默认启用)
echo 1 > /sys/module/zswap/parameters/enabled
# 设置最大内存占用(建议 20-25%)
echo 25 > /sys/module/zswap/parameters/max_pool_percent
# 选择压缩算法
# 低 CPU 开销场景:
echo lzo-rle > /sys/module/zswap/parameters/compressor
# 高压缩比场景:
echo zstd > /sys/module/zswap/parameters/compressor
# 设置接受阈值(池满后需降到多少才重新接受)
echo 90 > /sys/module/zswap/parameters/accept_threshold_percent# 查看当前统计
cat /sys/kernel/debug/zswap/pool_total_size # 压缩后总字节数
cat /sys/kernel/debug/zswap/stored_pages # 存储的页面数
# 计算压缩比
STORED=$(cat /sys/kernel/debug/zswap/stored_pages)
POOL_SIZE=$(cat /sys/kernel/debug/zswap/pool_total_size)
echo "压缩比 = $STORED * 4096 / $POOL_SIZE"
# 查看命中率(从 vmstat)
grep -E "^zswp" /proc/vmstat
# zswpin: N -- 从 zswap 加载(命中)
# zswpout: N -- 写入 zswap
# zswpwb: N -- 写回到 swap 设备(命中 = zswpin, 写回 = zswpwb)# 查看某容器的 zswap 使用量(cgroup v2)
cat /sys/fs/cgroup/some_container/memory.zswap.current
# 禁用某容器的 zswap writeback(避免影响 swap 设备)
echo 0 > /sys/fs/cgroup/some_container/memory.zswap.writeback
# 设置 zswap 使用上限
echo $((512 * 1024 * 1024)) > /sys/fs/cgroup/some_container/memory.zswap.max# 加载模块(可指定设备数量)
modprobe zram num_devices=2
# 设置压缩算法(必须在设置 disksize 之前)
echo lz4 > /sys/block/zram0/comp_algorithm
# 设置设备大小(建议为物理内存的 50-150%)
echo $((4 * 1024 * 1024 * 1024)) > /sys/block/zram0/disksize # 4GB
# 设置物理内存上限(防止 zram 占用过多真实内存)
echo $((2 * 1024 * 1024 * 1024)) > /sys/block/zram0/mem_limit # 2GB
# 初始化并挂载为 swap
mkswap /dev/zram0
swapon /dev/zram0 -p 100 # 优先级 100# 配置两级压缩:主算法 lz4(快速),辅算法 zstd(高压缩比)
echo lz4 > /sys/block/zram0/comp_algorithm
echo "algo=zstd priority=1" > /sys/block/zram0/recomp_algorithm
# 定期触发 idle 页面再压缩(适合在系统低负载时运行)
echo all > /sys/block/zram0/idle # 标记所有页为 idle
echo "type=idle" > /sys/block/zram0/recompress # 仅再压缩 idle 页
# 或者只再压缩巨大(不可压缩)页面
echo "type=huge" > /sys/block/zram0/recompress# 设置后备设备(用于存放 idle/huge 页面)
echo /dev/sdb > /sys/block/zram0/backing_dev
# 设置写回批次大小(默认 32 页)
echo 64 > /sys/block/zram0/wb_batch_size
# 启用写回限速(避免过于频繁写回)
echo 1 > /sys/block/zram0/wb_limit_enable
echo $((100 * 1024)) > /sys/block/zram0/wb_limit # 最多 100K * 4KB = 400MB
# 执行写回
echo idle > /sys/block/zram0/writeback # 写回 idle 页
echo huge > /sys/block/zram0/writeback # 写回不可压缩页
echo all > /sys/block/zram0/writeback # 写回所有符合条件的页# 综合统计
cat /sys/block/zram0/mm_stat
# 格式:
# orig_data_size(未压缩总大小)
# compr_data_size(压缩后总大小)
# mem_used_total(实际占用物理内存,含 zsmalloc 元数据)
# mem_limit(内存上限)
# mem_used_max(历史峰值)
# same_pages(全同页数量)
# pages_stored(当前存储页数)
# huge_pages(不可压缩页数)
# 计算压缩比
python3 -c "
import subprocess
out = subprocess.check_output(['cat', '/sys/block/zram0/mm_stat']).decode()
fields = out.split()
ratio = int(fields[0]) / int(fields[1])
print(f'压缩比: {ratio:.2f}:1')
"zsmalloc 内部规整可以通过 sysfs 手动触发(zram_drv.c 第 1876-1888 行):
# 手动触发 zsmalloc 内部规整(合并稀疏 zspage,释放物理页)
echo 1 > /sys/block/zram0/compact
# 查看 zsmalloc 分配器状态(需要 CONFIG_ZSMALLOC_STAT)
ls /sys/kernel/debug/zsmalloc/
# 每个 zram 设备和 zswap pool 对应一个目录
cat /sys/kernel/debug/zsmalloc/zram0/classes
# 显示每个 size class 的对象数、利用率、zspage 数量等CONFIG_ZSMALLOC_CHAIN_SIZE 控制单个 zspage 最多由几个物理页组成(第 99 行:#define ZS_MAX_PAGES_PER_ZSPAGE)。
- 值越大:zspage 可以存放更多对象,利用率更高;但单个 zspage 占用更多连续(逻辑上连续的 zpdesc 链表)内存
- 值越小:zspage 更小,更容易分配;但小对象的利用率下降
通常保持默认值(4)即可。在内存碎片严重的系统上,可适当减小以便于 zspage 分配。
# 使用 fio 测试 zram 写入吞吐量(作为 swap 设备)
fio --name=zram_test \
--filename=/dev/zram0 \
--rw=write \
--bs=4k \
--numjobs=4 \
--iodepth=32 \
--time_based \
--runtime=60 \
--output-format=json
# 使用 vm_stat 观察 zswap 命中率(运行内存压力测试时)
watch -n 1 "grep -E 'zswp' /proc/vmstat"# 使用 stress-ng 制造内存压力,观察 zswap/zram 效果
stress-ng --vm 4 --vm-bytes 80% --timeout 60s &
# 观察 zswap 存储量变化
watch -n 2 "
echo 'zswap stored_pages:' $(cat /sys/kernel/debug/zswap/stored_pages)
echo 'zswap pool_size(MB):' $(($(cat /sys/kernel/debug/zswap/pool_total_size) / 1048576))
"
# 观察 zram 压缩比变化
watch -n 2 "
read orig comp rest < /sys/block/zram0/mm_stat
echo \"zram ratio: $(python3 -c \"print(f'{$orig/$comp:.2f}:1' if $comp else 'N/A')\")\
"# 快速对比不同算法的压缩比(不适合生产环境)
for algo in lzo lzo-rle lz4 lz4hc zstd; do
# 重置 zram0 并换算法
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset
echo $algo > /sys/block/zram0/comp_algorithm
echo $((2*1024*1024*1024)) > /sys/block/zram0/disksize
mkswap /dev/zram0 && swapon /dev/zram0
# 制造负载
stress-ng --vm 2 --vm-bytes 70% --timeout 30s 2>/dev/null
# 读取结果
read orig comp rest < /sys/block/zram0/mm_stat
echo "$algo: ratio=$(python3 -c \"print(f'{$orig/$comp:.2f}')\" 2>/dev/null)"
done# zram as swap(推荐:性能好,无额外存储需求)
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm
echo $((MEMSIZE * 2)) > /sys/block/zram0/disksize # disksize = 2x 物理内存
mkswap /dev/zram0 && swapon /dev/zram0 -p 10
# 禁用 zswap(避免双重压缩)
echo 0 > /sys/module/zswap/parameters/enabled# zswap 作为 SSD swap 的前端缓存
echo 1 > /sys/module/zswap/parameters/enabled
echo lz4 > /sys/module/zswap/parameters/compressor
echo 20 > /sys/module/zswap/parameters/max_pool_percent
# SSD 作为 swap 设备
mkswap /dev/nvme0n1p3 && swapon /dev/nvme0n1p3# zram 作为 swap,优先 lzo-rle(CPU 友好)
echo lzo-rle > /sys/block/zram0/comp_algorithm
echo $((MEMSIZE)) > /sys/block/zram0/disksize # disksize = 1x 物理内存
# 配置 zram writeback 到 eMMC(用于 huge 页面)
echo /dev/mmcblk0p5 > /sys/block/zram0/backing_dev# 在宿主机启用 zswap,并为每个容器设置 zswap 限制(cgroup v2)
echo 1 > /sys/module/zswap/parameters/enabled
echo zstd > /sys/module/zswap/parameters/compressor # 高压缩比
# 在 Kubelet 配置中设置 memory.zswap.max
# 防止单个容器耗尽 zswap 池
echo $((256 * 1024 * 1024)) > /sys/fs/cgroup/kubepods/.../memory.zswap.max zswap 相关配置:
├── CONFIG_ZSWAP -- 启用 zswap
├── CONFIG_ZSWAP_DEFAULT_ON -- 默认启用 zswap
├── CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4 -- 默认使用 lz4
├── CONFIG_ZSWAP_COMPRESSOR_DEFAULT_ZSTD -- 默认使用 zstd
└── CONFIG_ZSWAP_ZPOOL_DEFAULT_ZSMALLOC -- 使用 zsmalloc(当前唯一选项)
zram 相关配置:
├── CONFIG_ZRAM -- 启用 zram 块设备
├── CONFIG_ZRAM_BACKEND_LZO -- lzo/lzo-rle 后端
├── CONFIG_ZRAM_BACKEND_LZ4 -- lz4 后端
├── CONFIG_ZRAM_BACKEND_LZ4HC -- lz4hc 后端
├── CONFIG_ZRAM_BACKEND_ZSTD -- zstd 后端
├── CONFIG_ZRAM_BACKEND_DEFLATE -- deflate 后端
├── CONFIG_ZRAM_BACKEND_842 -- 842(IBM Power)后端
├── CONFIG_ZRAM_DEF_COMP -- 默认压缩算法(字符串)
├── CONFIG_ZRAM_WRITEBACK -- 启用 writeback 功能
├── CONFIG_ZRAM_MULTI_COMP -- 启用多算法再压缩
├── CONFIG_ZRAM_TRACK_ENTRY_ACTIME -- 追踪访问时间(用于精细 idle 标记)
└── CONFIG_ZRAM_MEMORY_TRACKING -- debugfs 内存追踪
zsmalloc 相关配置:
├── CONFIG_ZSMALLOC -- 启用 zsmalloc(由 zswap/zram 自动选择)
├── CONFIG_ZSMALLOC_CHAIN_SIZE -- zspage 最大页数(默认 4)
└── CONFIG_ZSMALLOC_STAT -- 启用 debugfs 统计
通用内存压缩配置:
├── CONFIG_CRYPTO_LZ4 -- lz4 加密框架实现(zswap 使用)
├── CONFIG_CRYPTO_ZSTD -- zstd 加密框架实现(zswap 使用)
├── CONFIG_LZ4_COMPRESS -- lz4 直接接口(zram 使用)
├── CONFIG_LZO_COMPRESS -- lzo 直接接口(zram 使用)
└── CONFIG_ZSTD_COMPRESS -- zstd 直接接口(zram 使用)
注意:zswap 使用内核 crypto 框架(异步压缩接口 crypto_acomp),而 zram 使用直接压缩库接口(同步,无 crypto 框架开销)。这是两者架构上的重要差异,也是 zram 在低延迟场景下通常更快的原因之一。
| 函数 | 行号 | 说明 |
|---|---|---|
zs_create_pool |
2059 | 创建 zsmalloc pool |
zs_destroy_pool |
- | 销毁 pool(含所有 zspage) |
zs_malloc |
1297 | 分配对象,返回 handle |
zs_free |
1384 | 释放对象 |
zs_obj_write |
1176 | 向 handle 写入数据(支持跨页) |
zs_obj_read_sg_begin |
1114 | 开始读取对象(返回 SG 列表) |
zs_obj_read_sg_end |
1161 | 结束读取(释放 zspage 读锁) |
zs_obj_read_begin |
- | 返回连续指针(跨页时使用 local_copy) |
zs_obj_read_end |
- | 结束读取,释放锁或 local_copy |
zs_huge_class_size |
1238 | 返回 huge class 的阈值大小 |
zs_get_total_pages |
- | 返回 pool 当前占用的物理页数 |
zs_lookup_class_index |
- | 由对象大小反查 size class 索引(再压缩判断用) |
zs_compact |
- | 手动触发 zsmalloc 内部规整 |
get_size_class_index |
470 | 由对象大小计算 size class 索引 |
get_fullness_group |
624 | 计算 zspage 的 fullness 分组 |
fix_fullness_group |
683 | 调整 zspage 的 fullness 分组 |
obj_to_location |
721 | obj 值解码为 (zpdesc, obj_idx) |
location_to_obj |
738 | (zpdesc, obj_idx) 编码为 obj 值 |
| 函数 | 行号 | 说明 |
|---|---|---|
zswap_store |
1482 | 主写入入口(swap out 路径) |
zswap_store_page |
1402 | 单页写入实现 |
zswap_load |
1588 | 主读取入口(swap in 路径) |
zswap_compress |
853 | 压缩页面并存入 zsmalloc |
zswap_decompress |
932 | 从 zsmalloc 解压到 folio |
zswap_writeback_entry |
988 | 将 zswap 条目写回 swap 设备 |
zswap_pool_create |
245 | 创建 zswap_pool |
shrink_memcg_cb |
1094 | LRU shrinker 回调(二次机会) |
zswap_shrinker_count |
1196 | 计算可回收条目数 |
shrink_worker |
1312 | 后台 shrink 工作队列处理函数 |
zswap_setup |
1772 | 模块初始化 |
swap_zswap_tree |
231 | 由 swp_entry 找到对应 xarray |
acomp_ctx_get_cpu_lock |
- | 获取当前 CPU 压缩上下文(加锁) |
zswap_cpu_comp_prepare |
- | CPU online 时分配压缩上下文 |
zswap_cpu_comp_dead |
- | CPU offline 时释放压缩上下文 |
zswap_memcg_offline_cleanup |
682 | memcg 下线清理 |
zswap_entry_free |
- | 释放 entry(含 zsmalloc 空间) |
zswap_invalidate |
- | 使 swap 条目无效(swapoff/invalidate 路径) |
| 函数 | 行号 | 说明 |
|---|---|---|
zram_add |
3053 | 创建并注册 zram 块设备 |
zram_bio_write |
2760 | 处理写 bio |
zram_bio_read |
2729 | 处理读 bio |
zram_write_page |
2252 | 单页写入(压缩 + 存储) |
zram_read_page |
2141 | 单页读取(解压 + 返回) |
read_from_zspool |
2129 | 从 zsmalloc 读取(分类处理) |
write_same_filled_page |
2196 | 全同页面写入优化 |
write_incompressible_page |
2211 | 不可压缩页面写入 |
zram_writeback_slots |
1048 | 批量写回到后备设备 |
zram_writeback_complete |
918 | 处理写回完成(更新 slot 状态) |
zram_writeback_endio |
977 | bio 完成中断回调 |
recompress_slot |
2395 | 对单个 slot 执行再压缩 |
scan_slots_for_recompress |
2348 | 扫描符合再压缩条件的 slot |
slot_lock |
94 | 获取 slot 的 bit-lock |
slot_unlock |
103 | 释放 slot 的 bit-lock |
slot_trylock |
81 | 尝试获取 slot 锁(不等待) |
| 函数 | 行号 | 说明 |
|---|---|---|
zcomp_create |
228 | 创建 zcomp 实例(含 per-CPU 流初始化) |
zcomp_destroy |
220 | 销毁 zcomp 实例 |
zcomp_stream_get |
110 | 获取当前 CPU 的压缩流(加锁) |
zcomp_stream_put |
132 | 释放压缩流(解锁) |
zcomp_compress |
137 | 执行压缩 |
zcomp_decompress |
155 | 执行解压缩 |
zcomp_cpu_up_prepare |
169 | CPU online 时初始化压缩流 |
zcomp_cpu_dead |
181 | CPU offline 时释放压缩流 |
zcomp_available_show |
93 | 展示可用算法列表(sysfs 用) |
zcomp_available_algorithm |
87 | 检查算法名称是否有效 |
lookup_backend_ops |
75 | 按名称查找算法后端 |
由 Claude Code 分析生成