引言

在现代数据中心的Linux工作负载中,内存往往是第一瓶颈。当系统内存压力攀升时,传统的磁盘交换(swapping)虽然能避免OOM,但动辄数十毫秒的I/O延迟会让应用陷入"卡顿即死"的困境。为此,Linux内核社区在2.6时代引入了swap,后续又逐步演进出一套完整的内存压缩与交换子系统,核心成员包括 zswap(写回缓存)、zram(内存压缩块设备)和 zsmalloc(专用分配器),以及它们背后的基石——buddy system 大页伙伴系统。

本文将从源码级深度剖析这四大子系统的设计哲学、实现细节与调优实战,带你从页面生命周期走到压缩池管理,理解内核在"内存"与"CPU"之间那条精细的权衡曲线。


一、交换(Swap)基础:匿名页面的旅行

1.1 页面类型与换出路径

Linux内核按后端将物理页分为两类:

  • 文件页(Page Cache):由 mmap 映射或 read/write 缓冲产生,回写目标明确(对应文件 inode),回收成本低。
  • 匿名页(Anonymous Page):由 malloc / mmap(ANONYMOUS) 产生,无文件后端,必须写入 swap 设备才能释放。

换出匿名页的完整路径如下:

kswapd / direct reclaim
  └─ shrink_node()
       └─ shrink_list(LRU_INACTIVE_ANON)
            └─ shrink_page_list()
                 └─ pageout()
                      └─ get_swap_page()        // PTE 写入 swap entry
                      └─ swap_writepage()        // 构造 bio
                      └─ submit_bio()            // 写入 swap 设备

关键点:每个匿名页在 swap 中占据一个 saddr(swap address),由swap offset与swap type编码成64-bit PTE值。swap_info_struct内部维护swap_map引用位图与cluster(256页聚集写入单元),以减少磁盘寻道。

1.2 Swappiness 与回收偏好

vm.swappiness(0–200)控制匿名页在回收决策中的权重:

  • swappiness=0:几乎不回收匿名页(除非free page极低)。
  • swappiness=60:文件页与匿名页均衡回收(默认)。
  • swappiness=100:匿名页与文件页等权重。
  • swappiness=200:几乎只回收匿名页。

在cgroup v2中可分别设置memory.swap.high与memory.swap.max,实现容器级swap配额隔离。


二、zswap:交换路径上的压缩写回缓存

2.1 设计哲学

zswap 位于 swap 子系统后端,不暴露为独立块设备。当匿名页被换出时,swiper被迫先将页面交给 zswap_frontswap_store():

  1. 页面被压缩(默认 lzo-rle,可选 zstd/lz4)。
  2. 压缩体通过 zswap 专属分配器存入内存池。
  3. 只有当池满(max_pool_percent上限)时,最老条目才会"写回"到真正的 swap 设备。

这种"内存内缓存 + 磁盘写回"的架构,使得大多数 swap操作完全不触及磁盘。

2.2 核心数据结构

zpool            // 压缩后端分配器(zsmalloc/zbud/z3fold)
zswap_entry      // 压缩缓存条目
  ├─ offset       // swap entry offset(原磁盘地址)
  ├─ length       // 原始大小(通常4KB)
  ├─ compressed_size
  ├─ pool         // 属于哪个 pool
  └─ rb_node      // 红黑树节点(按 offset 排序)

zswap_tree       // 每 swap device 一棵红黑树

zbud是zswap早期默认后端,以两个压缩对象打包一个页("buddy-buddies")的方式存储,但压缩率只有50%上限。zsmalloc 是当前内核默认(5.x+),单个物理页可存储更多对象,理论空间利用率更高。

2.3 关键流程解读

换出路径(store):

frontswap_store(page)
  └─ zswap_store(page)
       ├─ compressor_compress(page_buf)     // 压缩
       ├─ zpool_alloc(zs_size)              // 从 zsmalloc 申请空间
       ├─ 写入压缩体
       └─ 超过 pool_limit 时 → zswap_writeback()
            └─ swap_writepage()              // 真写磁盘

换入路径(load):

frontswap_load(swap_entry)
  └─ zswap_load(swap_entry)
       ├─ 红黑树查找 entry
       ├─ zpool_read(entry, page)           // 解压
       └─ page 恢复为匿名页

2.4 关键参数

sysctl 参数说明实战建议
zswap.enabled总开关Y(云主机推荐)
zswap.max_pool_percent压缩池占系统总内存百分比20(大内存可升至25)
zswap.compressor压缩算法zstd(压缩率高)/ lzo(CPU低)
zswap.zpool分配器zsmalloc
zswap.accept_threshold_percent接受阈值(防拒绝压缩页风暴)90

2.5 压缩率实测

在多种工作负载中的典型压缩率:

工作负载类型zstdlz4lzo
Java 堆2.1×2.0×1.9×
Python 进程2.7×2.5×2.3×
Redis 内存1.3×1.2×1.1×
零页/稀疏页×100+×100+×100+

三、zsmalloc:专为压缩对象设计的分配器

3.1 问题背景

传统kmalloc最小分配单元32/64字节,内部碎片高。压缩后的对象大小不一(数百字节到4KB),需要高效利用单物理页存储多个对象。

3.2 尺寸类(Size Class)

zsmalloc 将可能对象大小划分为256个尺寸类(从2^7到2^12,每类间隔 ~sizeof(page_descriptor)),每个对象映射到唯一类。

一个 zpage(物理页)内的对象数计算:

max_objects = PAGE_SIZE / min_object_size(256B) = 16个(理想上限)
典型对象大小1KB时 → 4个/page
典型对象大小512B时 → 8个/page
典型对象大小256B时 → 16个/page

单个对象最大支持 ZS_SIZE_CLASS_DELTA 边界内的任意尺寸,实际限制为 PAGE_SIZE * 3/4(约3KB),超过则无法打包。

3.3 映射算法(zs_map_object)

存储时记录class index + obj index,查找时:

obj_offset = class->size * obj_idx + header
obj_addr   = zs_map_object(mapping_mode=MAPPING_READ)
  └─ kmap_atomic()  # 如 MAPPING_ATOMIC
  └─ vm_map_ram()    # 如 MAPPING_PGTABLE

跨页对象(zs_object_is_cached() 返回 false)使用"frag"算法:虚拟地址从两个连续物理页分别读取。

3.4 迁移与碎片整理

zsmalloc 不支持物理搬移(因为是压缩数据),但通过shrinker接口可以释放空闲 zpage回伙伴系统。内核mm/zsmalloc.c通过zs_shrinker注册,在内存压力时释放 pages_full 或 pages_compacted 中计数最少的 class。


四、zram:内存中的压缩交换块设备

4.1 与 zswap 的本质区别

维度zswapzram
定位swap 的缓存层独立块设备
暴露接口不可见/dev/zramN(见 swapon -s)
回写能力有(池满→磁盘 swap)无(纯内存)
压缩算法选择compressorcomp_algorithm(可热切换)
推荐场景有磁盘 swap 加速无 swap 分区/只希望用内存

4.2 关键参数

modprobe zram num_devices=1
echo 16G > /sys/block/zram0/disksize
echo zstd > /sys/block/zram0/comp_algorithm
mkswap /dev/zram0 && swapon /dev/zram0 -p 32767
注意:同时使用 zram + 磁盘 swap 时,建议将 zram 的 swapon priority 设最高(-p 32767),让系统优极zram。zo系权则容见于OOM时zram均满。

4.3 性能监控

cat /sys/block/zram0/mm_stat
#输出: orig_data_size compr_data_size mem_used_total ...
#压缩率 = orig_data_size / compr_data_size

4.4 zram writeback(v5.6+)

新版本 zram 支持WRITEBACK特性,能将冷数据写出到磁盘设备:

echo /dev/sdb1 > /sys/block/zram0/backing_dev
echo all > /sys/block/zram0/idle     # 标记全页为 idle
echo idle > /sys/block/zram0/writeback # 触发回写

这实现了"热数据驻内存压缩、冷数据落磁盘"的分层存储。


五、大页伙伴系统(Buddy with Compound Pages)

5.1 伙伴系统的演进

传统伙伴系统按order(0–10)管理 4KB–4MB 连续物理页。为支持大页(HugeTLB 2MB/1GB),内核引入复合页(compound page)机制:

  • order-N 的复合页占用 2^N 个连续物理页
  • page[0] 作为 head page,存储完整元数据
  • page[1..2^N-1] 作为 tail page,指向 head
  • PG_head/PG_tail页标志区分角色

5.2 split 与 coalescing

分配路径可能发生分裂(split):order-9 → order-8 × 2 → order-7 × 4。释放时通过__free_one_page检查相邻页是否空闲,触发合并(coalesce)提升order。

大页分配失败时的回退策略:

alloc_pages(GFP_TRANSHUGE)
  └─ 激进 reclaimed + kcompactd 规整
  └─ 仍失败 → fallback to 4KB base page + khugepaged 后期合并(SHMEM + THP)

5.3 per-CPU pageset 与冷热页

现代伙伴系统维护per_cpu_pageset:

  • 热页(hot):刚从伙伴系统取出,大概率被CPU cache命中(首次访问即可用)。
  • 冷页(cold):回收后暂存,cache line可能已失效,适合大批量DMA场景。
  • pcp->count 维护单 CPU 128页以内的快速得/放。
  • 当跨越high水位批量补给伙伴系统,低于batch从伙伴系统取水。

六、综合调优:zswap + swappiness 协同

6.1 参数组合矩阵

场景swappinesszswap.max_pool_percentzram disksize
Web 服务器(I/O密集)10–2020不推荐(争抢内存)
Redis/内存数据库0(禁用swap)10禁用
AI推理(显存映射)525禁用
嵌入式(无磁盘)60不启用RAM的 50%
zram+zswap混用3010RAM的 25%

6.2 监控与诊断命令

# 查看zswap池状态
cat /sys/kernel/debug/zswap/pool_total_size

# 查看zswap命中率(需tracepoint)
echo 1 > /sys/kernel/debug/tracing/events/zswap/enable

# 查看swap分布
swapon --show=NAME,TYPE,USED,PRIO

# PSI(Pressure Stall Information)内存压力
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=...

# Cgroup v2 容器级监控
cat /sys/fs/cgroup/memory.swap.current
cat /sys/fs/cgroup/memory.stat | grep swap

6.3 OOM 与 zswap 的互动

当压缩池也满时的死亡螺旋:

  1. 匿名页换出受阻(zram满 + 磁盘swap满)
  2. out_of_memory() 扫描全队列
  3. 选择 oom_score 最高进程KILL
  4. panic_on_oom=2 时直接重启

防御策略:

  • vm.overcommit_memory=2严格模式
  • memory.limit_in_bytes cgroup 硬限制
  • 配置oom_score_adj(如 systemd 的 OOMPolicy=stop)

七、内核源码精读:换页路径追踪

以下代码片段节选自mm/vmscan.c(v6.8+),展示kswapd的典型换出逻辑:

static unsigned long shrink_inactive_list(unsigned long nr_to_scan,
                                          struct lruvec *lruvec,
                                          struct scan_control *sc,
                                          enum lru_list lru)
{
    LIST_HEAD(page_list);
    unsigned long nr_reclaimed = 0;
    unsigned long nr_taken;
    // 1. 从 LRU_INACTIVE 链表摘取页面
    nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list, &nr_scanned, sc, lru);
    // 2. 批量回写
    nr_reclaimed = shrink_page_list(&page_list, pgdat, sc, &stat, false);
    // 3. 把未成功回写的页面放回active链表
    putback_inactive_pages(lruvec, &page_list);
    // 4. 统计reclaim效率
    // ...
}

zswap 参与时的差异点:shrink_page_list中调用的pageout()会优先走frontswap_store()(见__swap_writepage()),失败后才走磁盘。

关键判断逻辑:

if (frontswap_enabled && __swap_writepage(page, wbc, pageout_io)) {
    // zswap 承担,返回后继续
} else {
    // 传统磁盘 swap 路径
    swap_writepage(page, wbc);
}

八、总结与展望

Linux内存压缩与交换子系统是一条精密的管道:从进程匿名页出发,经过LRU扫描、压缩、写回伙伴系统提供的物理页帧、最终才触及磁盘。任何一环阻塞都会传递到整个系统。掌握这套机制,才能真正理解服务器在高内存压力下的行为:

  • zswap 解决的是"磁盘swap太慢"——用CPU换I/O延迟
  • zram 解决的是"没有swap分区"——用压缩率换容量
  • zsmalloc 解决的是"碎片"——用定制分配器压缩对象
  • buddy compound 解决的是"大页连续物理分配"——用复合页降TLB miss

未来方向:

  1. eBPF 感知的 page reclaim 策略(PSI 反馈动态调节 swappiness)
  2. CXL 异构内存下 zswap 冷热分层
  3. zram writeback 与 NVMe ZNS 硬件协同
  4. Rust for Linux 在 swap 子系统的新模块实验(drivers/swap迁移)

参考资料:Linux kernel v6.8mm/zswap.c、mm/zsmalloc.c、mm/zram、mm/vmscan.c、Documentation/admin-guide/mm/zswap.rst、《Understanding Linux Kernel》第8/9章。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部