Skip to content

Latest commit

 

History

History
2787 lines (2198 loc) · 105 KB

File metadata and controls

2787 lines (2198 loc) · 105 KB

Linux 内核压缩内存机制深度分析:zswap / zram / zsmalloc

基于 Linux 内核源码(主线 master 分支)实地分析

主要涉及文件:

  • mm/zswap.c(1845 行)
  • mm/zsmalloc.c(2258 行)
  • drivers/block/zram/zram_drv.c(3325 行)
  • drivers/block/zram/zram_drv.h
  • drivers/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 行)

目录

  1. 概述:三层压缩内存体系
  2. 整体架构图
  3. zsmalloc:专用压缩内存分配器
  4. zswap:交换前端压缩缓存
  5. zram:压缩 RAM 块设备
  6. 三者的关系与协作
  7. 锁机制分析
  8. 性能调优参数
  9. 统计与可观测性
  10. 常见问题与陷阱
  11. zswap 内存池演进:zbud、z3fold、zsmalloc
  12. zram 压缩算法深度剖析
  13. zram 多流压缩:per-CPU 压缩流机制
  14. zram 再压缩(recompress)机制
  15. zsmalloc handle 映射机制深度剖析
  16. zram writeback 深度分析
  17. zswap writeback 与驱逐策略深度分析
  18. 性能调优最佳实践与基准测试
  19. 内核配置选项全览
  20. 附录:关键函数索引

1. 概述:三层压缩内存体系

Linux 内核中存在两条独立但可协作的压缩内存路径:

用途分类:

  zswap  -- 透明地拦截 swap out 路径,作为 swap 设备的前端缓存
  zram   -- 注册为一个块设备 /dev/zramN,用户配置为 swap 分区或 tmpfs

两者都使用 zsmalloc 作为底层的内存分配器来保存压缩后的数据。zsmalloc 专门为存储大量大小不一的压缩对象而设计,避免了 slab/slub 分配器对小对象的内碎片问题。

在内核 6.x 之前,zswap 还支持通过 zpool 抽象层连接 zbud(每页 2 对象)或 z3fold(每页 3 对象)后端。而在当前主线版本中,zswap 已经直接使用 zsmallocmm/zswap.c 第 154 行:struct zs_pool *zs_pool),zpool 接口已被移除。


2. 整体架构图

2.1 系统级全景

  进程 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    |   两者共同使用的底层分配器
  |  内存分配器  |
  +-------------+

2.2 zswap 在内核内存路径中的位置

  进程访问内存
      |
      | 缺页异常 (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)
  +------------------+

2.3 zsmalloc 内部结构

  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 索引)

3. zsmalloc:专用压缩内存分配器

源码文件mm/zsmalloc.c

3.1 设计目标与约束

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)的倍数。

3.2 核心数据结构

zs_pool(第 199-220 行)

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 的迁移操作。

size_class(第 160-174 行)

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。

zspage(第 261-274 行)

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 链表)聚合为一个逻辑分配区域。

link_free(第 182-194 行)

struct link_free {
    union {
        unsigned long next;    /* 空闲对象:下一个空闲对象的索引 */
        unsigned long handle;  /* 已分配对象:存储 handle */
    };
};

空闲对象内嵌入空闲链表指针,这是一种经典的侵入式空闲链表设计。

3.3 size class 与 fullness group 机制

size class 分配策略(第 120-122 行)

#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_zspageobjs_per_zspage 相同时,让较小的 class 复用较大的 class,减少不必要的内存碎片。

fullness group(第 134-141 行)

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 行)。

3.4 zspage 内存布局

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 的原因——防止在跨页读写时页面被迁移。

3.5 handle 编码方案

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 的调用方不受影响。

3.6 对象读写:跨页映射问题

zs_obj_write(第 1176-1223 行)

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);
}

zs_obj_read_sg_begin(第 1114-1159 行)

读路径使用 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 操作,避免中间拷贝。

3.7 分配与释放路径

zs_malloc(第 1297-1354 行)

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;
}

zs_free(第 1384-1416 行)

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 对象 */
}

3.8 内存规整(compaction)

zsmalloc 实现了内部规整(CONFIG_COMPACTION),通过注册 shrinker(第 209 行:struct shrinker *shrinker)来驱动。规整将稀疏 zspage 中的对象迁移到更密集的 zspage,然后释放空出来的物理页。

规整时需要更新 handle(将 obj 值写入 handle 指向的内存),这正是间接 handle 设计的价值所在。规整操作受 pool->lock(写锁)和 zspage->zsl(写锁)双重保护,因此正常的读写路径持有读锁即可并发执行。


4. zswap:交换前端压缩缓存

源码文件mm/zswap.c

4.1 核心设计思路

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.

关键设计决策

  1. zswap 不拦截 swap in(读取)——swap in 时通过 zswap_load 直接从 RAM 解压,无需磁盘 IO
  2. 当内存压力大时,通过 shrinker 将 zswap 中的旧条目 writeback 到 swap 设备,为新条目腾出空间
  3. 使用 xarray(而非之前版本的红黑树)作为索引结构,key 为 swap 偏移量
  4. per-memcg LRU:支持 cgroup 级别的内存限制和淘汰策略

4.2 核心数据结构

zswap_entry(第 190-198 行)

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 中,占用约一个物理页)。

zswap_pool(第 153-161 行)

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 引用归零后异步销毁。

crypto_acomp_ctx(第 139-145 行)

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 行)。

全局索引:zswap_trees(第 200 行)

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];
}

4.3 写入路径:zswap_store

写入路径在 mm/page_io.c:275 被调用:if (zswap_store(folio)),在实际写入 swap 设备前尝试压缩存入 zswap。

zswap_store(第 1482-1566 行)

  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(第 1402-1480 行)

  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 路径上触发内存回收死锁。

4.4 读取路径:zswap_load

zswap_load(第 1588-1646 行)

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)

4.5 压缩/解压实现

zswap_compress(第 853-930 行)

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;
    ...
}

zswap_decompress(第 932-971 行)

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 的物理页读取,无需先复制到连续内存。

4.6 Shrinker:内存压力淘汰

zswap 的 shrinker 实现了一个精心设计的二次机会(second chance)算法,同时考虑 swap in 速率和压缩比。

shrink_memcg_cb(第 1094-1173 行)

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;
    }
    ...
}

zswap_shrinker_count(第 1196-1266 行)

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 的过度淘汰。

writeback 流程(第 988-1067 行)

  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 设备

4.7 cgroup 感知机制

zswap 支持完整的 cgroup v2 感知(CONFIG_MEMCG):

  • per-memcg 计费obj_cgroup_charge_zswap(objcg, entry->length) 在压缩成功时收费,obj_cgroup_uncharge_zswap 在释放时退费(第 722-724 行)
  • per-memcg LRUzswap_list_lru 使用 list_lru_init_memcg 初始化,支持 per-node、per-memcg 的独立 LRU(第 1798 行)
  • memcg 限制检查obj_cgroup_may_zswap(objcg) 检查此 memcg 的 zswap 使用量是否超限
  • memcg writeback 控制memory.zswap.writeback cgroup 接口可以禁用特定 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);
}

4.8 pool 管理与生命周期

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)

4.9 初始化流程

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);

5. zram:压缩 RAM 块设备

源码文件drivers/block/zram/zram_drv.cdrivers/block/zram/zram_drv.h

5.1 定位与使用场景

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)

5.2 核心数据结构

zram_table_entry(zram_drv.h,第 66-78 行)

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 行),因此数据大小最大可编码到一页以上,预留了标志位空间。

zram(zram_drv.h,第 108-146 行)

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
};

zram_stats(zram_drv.h,第 80-96 行)

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
};

5.3 块设备注册与初始化

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 的内存操作特性相符。

5.4 写入路径:zram_bio_write

  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 填充。

5.5 读取路径:zram_bio_read

  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
                                             (解压缩)

5.6 特殊页面处理

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
                     读取:解压缩

5.7 Writeback 到后备设备

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),读回时需要再次解压。

5.8 多压缩算法支持(MULTI_COMP)

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)。


6. 三者的关系与协作

6.1 协作方式

  典型场景:zswap 在前,/dev/zram0 在后

  进程内存                     zswap                     zram (/dev/zram0)
  +----------+                +----------+                +----------+
  |  匿名页  |  swap out      |  压缩缓存 |  writeback     | 压缩块设备|
  | (4KB/页) |--------------->| (zsmalloc|--------------->| (zsmalloc|
  +----------+                |  handle) |                |  handle) |
       ^                      +----------+                +----------+
       |                           |                            |
       | swap in                   | zswap_load                 |
       | (无磁盘IO)                 | (命中时)                   |
       +---------------------------+                            |
       |                                                        |
       | swap in (zswap 未命中)                                 |
       +--------------------------------------------------------+

6.2 共用 zsmalloc

两者对 zsmalloc 的使用方式相同:

  • zswap_pool->zs_poolzs_create_pool("zswapXX") 创建
  • zram->mem_poolzs_create_pool("zramN") 创建(disksize_store 中调用)
  • 均使用 zs_malloc 分配,zs_obj_write 写入,zs_obj_read_sg_begin/end 读取,zs_free 释放

6.3 与 swap 子系统的集成点

操作 集成点 函数
swap out mm/page_io.c:275 zswap_store(folio)
swap in mm/swap_state.cdo_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)

7. 锁机制分析

7.1 zsmalloc 锁层次

代码注释明确定义了锁顺序(第 19-24 行):

  page_lock (struct page 的 PG_locked)
      |
  pool->lock (rwlock_t,保护 zspage 迁移)
      |
  class->lock (spinlock_t,保护 fullness_list 和 zspage 操作)
      |
  zspage->zsl (zspage_lock,保护跨页对象访问)

7.2 zswap 锁层次

代码注释(第 148-152 行):

  zswap_tree.lock (XArray 内部锁,保护 xarray 操作)
      |
  zswap_pool.lru_lock (list_lru 内部锁,保护 LRU 链表)

注:lru_lock 不需要在持有 tree.lock 的情况下才能获取(writeback 路径会在拿到 lru_lock 后再去 xa_load 验证)。

7.3 zram slot 锁

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),通过位操作实现锁。


8. 性能调优参数

8.1 zswap 参数

参数 路径 默认值 说明
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;
}

8.2 zram sysfs 接口

接口 说明
/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)

8.3 zsmalloc 调优

参数 位置 说明
CONFIG_ZSMALLOC_CHAIN_SIZE Kconfig 单个 zspage 最大页数(影响 huge class 阈值)
CONFIG_ZSMALLOC_STAT Kconfig 启用 debugfs 统计

zsmalloc 统计通过 debugfs 暴露(/sys/kernel/debug/zsmalloc/<pool_name>/classes),可以看到每个 size class 的对象分布和利用率。


9. 统计与可观测性

9.1 zswap debugfs

路径:/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 设备的页面数

9.2 zswap vm 事件

通过 /proc/vmstat 可见(第 1532-1534 行、第 1622 行):

ZSWPOUT   # zswap 写入(swap out 被 zswap 拦截)
ZSWPIN    # zswap 读取命中(无磁盘 IO)
ZSWPWB    # zswap writeback(淘汰到 swap 设备)

9.3 zram sysfs 统计

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

10. 常见问题与陷阱

10.1 zswap 与 zram 同时启用

组合使用时,zram 作为 swap 设备,zswap 作为 zram 的前端缓存:

  zswap -> zram (swap 设备)

此时 zswap 压缩 + zram 再压缩会有双重压缩开销,且 zram 已经在 RAM 中,zswap 的优势(减少磁盘 IO)不再明显。一般建议二选一,或者在内存极度受限时才组合使用。

10.2 不可压缩页面的处理

entry->length == PAGE_SIZE(zswap)或 ZRAM_HUGE 标志(zram)时,页面原样存储于 zsmalloc,占用一个物理页的空间(外加少量元数据)。在压缩比差的工作负载下,这会导致实际占用内存高于期望。

zswap 的处理策略:若 mem_cgroup_zswap_writeback_enabled 为 false(memcg 禁用 writeback),则直接拒绝不可压缩页面(第 895-900 行),避免无效占用内存。

10.3 内存分配失败的处理

zs_malloc 在 zswap 路径中使用 GFP_NOWAIT | __GFP_NORETRY(第 907 行),意味着分配失败时不会重试也不会触发内存回收。这是为了防止在 swap out 路径上触发递归回收造成死锁。失败时 zswap_store 返回 false,系统回退到直接写 swap 设备。

10.4 Large folio 的限制

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 目前的已知限制。

10.5 zspage 锁的特殊设计

zspage_lock 使用了一种非标准的读写锁设计(第 255-352 行):

  • 写锁:总是原子的,使用 trylock(不能 sleep)
  • 读锁:可以 sleep(因为只有原子写者)

这是因为 zspage 读锁可能在进程上下文中长期持有(在读/写对象期间),而写锁只用于内存规整(compaction),规整操作可以重试,因此接受 trylock 失败。

10.6 CPU hotplug 与压缩上下文

每个 zswap_poolcpuhp_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 行的自旋等待逻辑处理了极端竞争情况)。


11. zswap 内存池演进:zbud、z3fold、zsmalloc

理解 zswap 的演进历史有助于理解当前架构的设计决策。

11.1 历史:zpool 抽象层

在 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%,但实现复杂度大幅上升,且在某些边界情况下仍有显著的内部碎片。

11.2 当前:直接使用 zsmalloc

从 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 本地分配

11.3 zbud 设计回顾

虽然 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 字节被浪费。

11.4 z3fold 改进

z3fold 通过将每页分成三个插槽来改进利用率,但引入了复杂的 "folding" 和 "unfolding" 操作(将两个半满页合并为一个更满的页),这在压力下容易死锁,也是它最终被废弃的原因。

11.5 zsmalloc 的优势来源

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 在所有大小下均能保持高利用率。

12. zram 压缩算法深度剖析

源码文件drivers/block/zram/zcomp.cdrivers/block/zram/backend_*.c

12.1 zcomp 抽象层:统一压缩前端

zram 通过 struct zcomp_opszcomp.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 行)。

12.2 lzo / lzo-rle 后端

源码文件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 资源紧张的嵌入式设备

12.3 lz4 / lz4hc 后端

源码文件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_tLZ4_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 热路径  后台再压缩

12.4 zstd 后端(含字典支持)

源码文件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 工作内存,取决于级别)
  - 字典支持:是(可以为特定工作负载训练专用字典)
  - 适用场景:后台再压缩,对压缩比要求高的场景

12.5 deflate 与 842 后端

deflatebackend_deflate.c):基于 zlib 的经典 DEFLATE 算法,支持配置 winbits(窗口大小)参数。在 zcomp_params 中有专属的 struct deflate_paramszcomp.h 第 10-12 行)。deflate 的优势是标准性(与 gzip 兼容),但在速度和压缩比上均劣于 lz4 和 zstd。

842backend_842.c):IBM Power 处理器的硬件辅助压缩算法。当运行在 Power 平台时,内核可以利用 POWER NX(Nest Accelerator)硬件单元进行硬件加速压缩/解压,显著降低 CPU 开销。在非 Power 平台上退化为软件实现,性能一般。

  所有支持算法的综合对比:

  算法        压缩速度     解压速度     压缩比    内存开销   字典支持
  lzo-rle     极快         极快         中        极小       否
  lzo         极快         极快         中        极小       否
  lz4         极快         极快         中高      小         是
  lz4hc       中           极快         高        中         是
  zstd        中           快           很高      中         是
  deflate     慢           中           高        中         否
  842         快(hw)       快(hw)       中        小         否

12.6 算法选择策略与对比

选择压缩算法时需要权衡以下因素:

场景一:移动设备 / 嵌入式系统

  • 推荐: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_algorithm

13. zram 多流压缩:per-CPU 压缩流机制

13.1 per-CPU 流设计理念

zram 的并发性能关键在于 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 的缓冲区是为了处理"压缩失败"的情况:当压缩输出大于输入时,需要额外空间存放溢出数据,而不是直接写坏内存。

13.2 zcomp_stream_get/put 实现

zcomp_stream_getzcomp.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 能睡眠的情况下,不能持有禁止抢占的引用。竞争处理策略是:

  1. raw_cpu_ptr 获取当前 CPU 的流指针(此时可能被迁移)
  2. 尝试对该流加互斥锁(可能睡眠,期间可能发生 CPU 迁移)
  3. 加锁后检查 buffer 是否为 NULL(NULL 表示该 CPU 的流已被 cpu_dead 释放)
  4. 如果 buffer 为 NULL,解锁并重试(此时已经在新 CPU 上)

对应的释放函数极为简单(第 132-135 行):

void zcomp_stream_put(struct zcomp_strm *zstrm)
{
    mutex_unlock(&zstrm->lock);
}

13.3 CPU 热插拔处理

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_freebuffer 设为 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 是不同的状态位。

13.4 压缩缓冲区布局

  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 缓冲区再进行解压缩。


14. zram 再压缩(recompress)机制

CONFIG_ZRAM_MULTI_COMP 引入了再压缩功能,允许使用优先级更高(通常压缩比更高)的算法重新压缩已有数据。

14.1 多优先级压缩设计

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),用于再压缩时的进度追踪。

14.2 recompress_slot 执行流程

核心函数 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 时,才能实际节省内存。

14.3 post-processing 控制结构

为了避免在扫描所有 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 过程中持有大粒度锁。


15. zsmalloc handle 映射机制深度剖析

15.1 handle 间接寻址的完整链路

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)不需要任何修改。

15.2 obj 值编码的位域结构

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 区(存储下一个空闲对象的索引)。

15.3 跨页对象的 kmap 策略

当压缩对象跨越两个物理页时,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 建立临时映射。

15.4 zs_obj_read_begin 接口

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 中)。这种设计使调用方无需关心是否跨页。


16. zram writeback 深度分析

16.1 writeback 控制结构

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 中)

16.2 异步 bio 提交与完成处理

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 行)中完成,在进程上下文中执行。

16.3 compressed_wb 模式

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 行保存这些信息)。

16.4 访问时间追踪与 idle 标记

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 内存压缩管理。


17. zswap writeback 与驱逐策略深度分析

17.1 LRU 结构:list_lru 的 per-node、per-memcg 设计

zswap 使用内核的 struct list_lrumm/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 回调处理每个条目。

17.2 shrinker 触发时机

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;
}

17.3 shrink_worker 后台线程

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_workershrink_wqWQ_UNBOUND | WQ_MEM_RECLAIM)工作队列上执行,WQ_MEM_RECLAIM 确保即使在内存压力下也能分配执行线程。

17.4 writeback 并发安全分析

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 对象。


18. 性能调优最佳实践与基准测试

18.1 zswap 调优指南

基础参数配置

# 启用 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

监控 zswap 效果

# 查看当前统计
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)

cgroup 级别控制

# 查看某容器的 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

18.2 zram 调优指南

初始化 zram 设备

# 加载模块(可指定设备数量)
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

MULTI_COMP 再压缩配置

# 配置两级压缩:主算法 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

Writeback 配置

# 设置后备设备(用于存放 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       # 写回所有符合条件的页

查看 zram 状态

# 综合统计
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')
"

18.3 zsmalloc 调优

内存规整

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 影响

CONFIG_ZSMALLOC_CHAIN_SIZE 控制单个 zspage 最多由几个物理页组成(第 99 行:#define ZS_MAX_PAGES_PER_ZSPAGE)。

  • 值越大:zspage 可以存放更多对象,利用率更高;但单个 zspage 占用更多连续(逻辑上连续的 zpdesc 链表)内存
  • 值越小:zspage 更小,更容易分配;但小对象的利用率下降

通常保持默认值(4)即可。在内存碎片严重的系统上,可适当减小以便于 zspage 分配。

18.4 基准测试方法

测量压缩吞吐量

# 使用 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

18.5 常见部署场景配置

场景一:云虚拟机(2-8 GB 内存)

# 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

场景二:桌面系统(8-32 GB 内存)

# 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

场景三:移动设备 / 嵌入式(512MB - 4GB 内存)

# 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

场景四:容器平台(Kubernetes)

# 在宿主机启用 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

19. 内核配置选项全览

  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 在低延迟场景下通常更快的原因之一。


20. 附录:关键函数索引

zsmalloc(mm/zsmalloc.c)

函数 行号 说明
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(mm/zswap.c)

函数 行号 说明
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(drivers/block/zram/zram_drv.c)

函数 行号 说明
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(drivers/block/zram/zcomp.c)

函数 行号 说明
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 分析生成