引言
在现代数据中心的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():
- 页面被压缩(默认
lzo-rle,可选zstd/lz4)。 - 压缩体通过 zswap 专属分配器存入内存池。
- 只有当池满(
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 压缩率实测
在多种工作负载中的典型压缩率:
| 工作负载类型 | zstd | lz4 | lzo |
|---|---|---|---|
| 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 的本质区别
| 维度 | zswap | zram |
|---|---|---|
| 定位 | swap 的缓存层 | 独立块设备 |
| 暴露接口 | 不可见 | /dev/zramN(见 swapon -s) |
| 回写能力 | 有(池满→磁盘 swap) | 无(纯内存) |
| 压缩算法选择 | compressor | comp_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
-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 参数组合矩阵
| 场景 | swappiness | zswap.max_pool_percent | zram disksize |
|---|---|---|---|
| Web 服务器(I/O密集) | 10–20 | 20 | 不推荐(争抢内存) |
| Redis/内存数据库 | 0(禁用swap) | 10 | 禁用 |
| AI推理(显存映射) | 5 | 25 | 禁用 |
| 嵌入式(无磁盘) | 60 | 不启用 | RAM的 50% |
| zram+zswap混用 | 30 | 10 | RAM的 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 的互动
当压缩池也满时的死亡螺旋:
- 匿名页换出受阻(zram满 + 磁盘swap满)
out_of_memory()扫描全队列- 选择 oom_score 最高进程
KILL panic_on_oom=2时直接重启
防御策略:
vm.overcommit_memory=2严格模式memory.limit_in_bytescgroup 硬限制- 配置
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
未来方向:
- eBPF 感知的 page reclaim 策略(PSI 反馈动态调节 swappiness)
- CXL 异构内存下 zswap 冷热分层
- zram writeback 与 NVMe ZNS 硬件协同
- 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章。

发表评论 取消回复