Linux 内核页面回收与交换子系统深度工程实战
从 LRU 链表到 kswapd,从 swap 到 zswap,从碎片整理到 OOM —— 全方位解析 Linux 内核如何管理"内存不够用"的问题。
一、为什么内存永远不够用
现代 Linux 服务器动辄配备数百 GB 内存,却依然会在高负载场景下遭遇内存压力。理解页面回收(Page Reclaim)和交换(Swap)子系统,是从"能用"到"好用"的必经之路。
内存分配请求并非总能成功。当空闲内存降至阈值( watermark )时,内核必须回收页面以腾出空间。这个过程如果处理不当,会导致系统卡顿、服务超时甚至触发 OOM Killer 杀死关键进程。
本文覆盖以下核心内容:
- 页面生命周期与 LRU 算法
- 直接回收 vs 异步回收(kswapd)
- Swap 子系统架构与性能权衡
- zswap / zram 压缩交换技术
- 内存碎片整理与页面迁移
- 实战调优参数与排错方法
二、页面生命周期与 LRU 算法
2.1 页面的"冷热"之分
Linux 内核维护两条 LRU(Least Recently Used)链表来区分页面的活跃程度:
- Active List(活跃链表):近期被访问过,暂不回收
- Inactive List(非活跃链表):近期未被访问,优先回收
一个页面最初进入 Inactive List。如果它在被回收前被再次访问(通过硬件缺页异常或软件标记),就会被提升到 Active List。这本质上是二次机会算法(Second Chance)的实现。
Page Access Timeline:
分配 ──→ Inactive List ──→ [被访问] ──→ Active List
│ │
[未被访问,扫到] [老化降级]
↓ ↓
回收出清 ←───────── Inactive List
2.2 文件页与匿名页的分化
内核对不同类型的页面采用不同的回收策略:
| 类型 | 来源 | 回收代价 | 回收方式 |
|---|---|---|---|
| 文件页(File Page) | 磁盘文件映射 | 低(丢弃或写回) | 直接丢弃脏页写回 |
| 匿名页(Anonymous Page) | malloc/mmap | 高(需写入 swap) | 写入 swap 设备 |
| Slab 页 | 内核对象缓存 | 中 | 释放对象 |
| 页缓存 | 文件读取缓冲 | 低 | 直接丢弃 |
文件页回收的代价远低于匿名页:干净的文件页可以直接丢弃(下次从磁盘重新读取),脏页写回磁盘即可;而匿名页没有后备存储,必须写入 swap 区域才能回收。
2.3 /proc/sys/vm/swappiness:回收偏好权衡
swappiness 控制内核在文件页和匿名页之间的回收倾向,取值 0-200:
swappiness=0:除非没有文件页,否则不回收匿名页(适合数据库服务器)swappiness=60:默认值,平衡回收swappiness=100:文件页和匿名页等比例回收swappiness=200:优先回收匿名页(适合桌面/交互场景)
实际生产中,高性能数据库(如 PostgreSQL、MySQL InnoDB)常将 swappiness 设为 1-10,以尽量减少冷数据页被换出。
三、回收触发机制:kswapd 与 Direct Reclaim
3.1 水位线(Watermark)体系
每个内存区域(Zone)设置三条水位线:
┌──────────────────────────────────────────────────┐
│ Zone Size │
│ │
├── HIGH ──────────────────────────────────────────┤ ← 充足,kswapd 休眠
│ │
├── LOW ───────────────────────────────────────────┤ ← kswapd 开始回收
│ │
└── MIN ───────────────────────────────────────────┘ ← 触发直接回收
(pages_min)
- HIGH:内存充裕,回收线程休眠
- LOW:唤醒 kswapd 异步回收
- MIN:触发直接回收(阻塞分配者)
3.2 kswapd:异步回收守护进程
每个 NUMA 节点运行一个 kswapd 守护进程。当空闲内存降至 LOW 水位时,kswapd 被唤醒,按预设步长(vm.min_free_kbytes)回收页面,直到恢复到 HIGH 水位。
kswapd 的工作流程:
kswapd 唤醒
│
├── 检查每个 Zone 的水位
│
├─→ Zone 内存 < LOW?
│ │
│ ├─Yes─→ 执行页面回收(按 swappiness 比例)
│ │ ├── 扫描 Inactive List
│ │ ├── 判断页面是否可回收
│ │ ├── 回收文件页 / 换出匿名页
│ │ └── 重复直到恢复到 HIGH
│ │
│ └─No─→ 继续下一个 Zone
│
└── 所有 Zone 充裕 → 进入睡眠
3.3 Direct Reclaim:同步直接回收
当分配请求到达时,若 MIN 水位以下的内存不足以满足分配,分配进程自身必须同步执行回收。这是性能杀手——业务线程被阻塞在内存回收上,导致延迟尖刺。
直接回收的典型症状:
sar -B中pgscank和pgscand高频增长iostat中 swap 设备写入量突增- 应用 P99 延迟飙升
避免 direct reclaim 的策略:
- 合理设置
vm.min_free_kbytes - 增加物理内存或减少内存超配
- 使用内存 Cgroup 限制内存使用
四、Swap 子系统深度剖析
4.1 Swap 的不可替代性
尽管常有人建议"禁用 swap",但 swap 实际提供多重价值:
- 容纳冷内存:长时间不用的匿名页可以被换出,腾出物理内存给热数据和文件缓存
- 防止 OOM:提供最后的安全网,避免直接杀进程
- 休眠(Hibernate)支持:内存镜像保存到 swap
- 内核准备换出匿名页
- 通过 zswap 压缩页面(默认使用 zstd/lz4 压缩)
- 压缩数据存入内存中的 zswap Pool
- 当 zswap Pool 达到上限(
max_pool_percent),或页面长时间未访问时,才写入底层 swap 设备 - 内存占用总量(RSS + Swap)
- 运行时间(越长得分越低)
- 是否持有重要资源(硬件设备、关键锁)
oom_score_adj调整值(-1000 到 +1000)- 以页粒度采样监控内存访问模式
- 为智能页面回收提供精确依据
- 替代启发式的 LRU 扫描
- LRU 算法管理页面冷热分类,swappiness 决定回收偏好
- kswapd 负责异步回收,direct reclaim 是最后的同步手段
- Swap 提供匿名页的换出空间,zswap/zram 用压缩换性能
- 内存规整解决外部碎片,为连续分配创造条件
- OOM Killer 是兜底机制,需要合理配置保护关键进程
真正的问题不是"要不要 swap",而是"swap 有多快"。
4.2 Swap 分区 vs Swap 文件
Linux 支持两种 swap 存储:
# Swap 分区:性能最优,无文件系统开销
mkswap /dev/nvme0n1p4
swapon /dev/nvme0n1p4
# Swap 文件:灵活,可扩展
dd if=/dev/zero of=/swapfile bs=1M count=8192
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
Swap 文件在现代内核(≥4.0)上性能接近分区,因为内核通过 swap_info_struct 中的Extent映射直接定位磁盘块,绕过了文件系统缓存层。
4.3 Swap Slot 分配算法
内核使用基于集群(Cluster)的 Swap 分配策略:
Swap Area Layout:
┌─────────┬───┬───┬───┬─────────┬───┬───┬───┬───┐
│ Cluster │ 0 │ 1 │ 2 │ Cluster │ 3 │ 4 │ 5 │ 6 │
│ A │ │ │ │ B │ │ │ │ │
└─────────┴───┴───┴───┴─────────┴───┴───┴───┴───┘
← 8 pages → ← 8 pages →
分配策略:从当前位置顺序分配,减少磁盘寻道
连续页面尽量分配到同一 Cluster
五、zswap 与 zram:压缩交换技术
5.1 zswap:压缩缓存层
zswap 不是独立的 swap 设备,而是在匿名页写入真正 swap 设备之前,先压缩存储在内存中的缓存层。
匿名页 → zswap 压缩 → [命中率高,大部分停留内存]
│
┌── 驱逐时 ─┘
↓
写入真正的 Swap 设备
zswap 的工作流程:
核心优势:大量的"轻度压力"场景下,页面只被压缩而不真正写磁盘,减少 I/O 压力。
5.2 zram:内存中的压缩块设备
zram 创建一个基于 RAM 的压缩块设备,可以作为 swap 设备使用:
# 创建 zram swap 设备
modprobe zram num_devices=1
echo zstd > /sys/block/zram0/comp_algorithm
echo 8G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0 -p 10 # 高优先级
zram 与 zswap 的对比:
| 特性 | zswap | zram |
|---|---|---|
| 本质 | 缓存层(前端) | 独立 swap 设备 |
| 后备存储 | 需要真实 swap 设备 | 无(压缩率在内存中) |
| 双重压缩风险 | 可能(zswap + swap 都用压缩) | 无 |
| 内存使用 | 动态(按使用率增长) | 固定分配 |
| 适合场景 | 有物理 swap 设备时叠加加速 | 无 physical swap / 嵌入式 |
5.3 压缩算法选择
zram/zswap 支持的压缩算法:
┌──────────┬────────────┬──────────┬────────────────┐
│ Algorithm│ Compression│ Speed │ Use Case │
├──────────┼────────────┼──────────┼────────────────┤
│ zstd │ 高 (2.8:1) │ 中等 │ 通用首选 │
│ lz4 │ 低 (2.1:1) │ 极快 │ 延迟敏感 │
│ lzo-rle │ 低 (2.0:1) │ 快 │ 嵌入式 │
│ zstd-le │ 较高 │ 中等 │ 较高压缩率优先 │
│ 842 │ 低 │ 快(Accel)│ 有硬件加速器 │
└──────────┴────────────┴──────────┴────────────────┘
建议:通用场景用 zstd,延迟敏感用 lz4
六、内存碎片整理与页面迁移
3.1 外部碎片问题
经过长时间运行后,物理内存可能散布着大量不连续的小页面块,导致无法满足大页(HugePage)或大内存块的连续分配请求。
内存碎片示意(长时间运行后):
[页:Used][页:Free][页:Used][页:Free][页:Used][页:Free]...
↑
没有连续的大块空闲内存
3.2 页面迁移(Page Migration)
内核通过内存规整(Memory Compaction)解决外部碎片:扫描内存区域,将可移动页面迁移到新位置,将空闲页面聚合为连续块。
规整前:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ U │ F │ U │ F │ M │ F │ M │ F │
└───┴───┴───┴───┴───┴───┴───┴───┘
F=Free U=Unmovable M=Movable
规整后:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ U │ M │ M │ F │ F │ F │ U │ F │
└───┴───┴───┴───┴───┴───┴───┴───┘
连续3个空闲页 ↑
关键代码路径(简化):
// mm/compact.c 核心逻辑
enum compact_result compact_zone(struct zone *zone, struct compact_control *cc)
{
// 1. 扫描 Zone,分类页面为 Movable/Unmovable/Free
// 2. 分离前后页面:将 Movable 页移到前端
// 3. 后端形成连续空闲块
// 3. 后端形成连续空闲块
}
3.3 Transparent Huge Pages(THP)与碎片
THP 依赖连续的物理内存(2MB / 1GB)。碎片整理能为 THP 创造条件。
THP 模式选择:
# 禁用 THP(某些数据库推荐)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 按需启用(默认 madvise)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 然后应用通过 madvise(MADV_HUGEPAGE) 标记需要巨页的区域
# 始终启用(激进,可能增加延迟)
echo always > /sys/kernel/mm/transparent_hugepage/enabled
实战建议:数据库类工作负载(MongoDB、PostgreSQL)通常建议禁用 THP,因为 THP 的延迟尖刺和碎片整理成本超过收益;而 HPC、大内存分析应用则受益于 THP。
七、OOM Killer:最后的防线
当所有回收手段耗尽,仍然无法满足分配请求时,OOM Killer 被触发。它根据评分算法选择"最该死"的进程杀掉。
7.1 OOM Score 计算
每个进程的 OOM 评分存储在 /proc/,计算基于:
7.2 保护关键进程
# 将关键进程标记为不可杀
echo -1000 > /proc/$(pidof your_critical_service)/oom_score_adj
# systemd 服务文件配置
[Service]
OOMPolicy=continue
OOMScoreAdjust=-900
7.3 OOM 事件分析
# 查看上次 OOM 事件
dmesg | grep -i "out of memory" | tail -20
# 查看 OOM Killer 日志
journalctl -k | grep "oom-kill"
八、实战调优:/proc/sys/vm 关键参数
8.1 核心参数速查表
| 参数 | 默认值 | 说明 | 建议 |
|---|---|---|---|
swappiness |
60 | 匿名页回收倾向 | DB: 1-10, Web: 30-60 |
min_free_kbytes |
自动计算 | 预留内存,避免 direct reclaim | 大内存机器按 ~0.5-1% 设置 |
vfs_cache_pressure |
100 | dentry/inode 缓存回收倾向 | 文件服务器: 50, DB: 150-200 |
dirty_ratio |
20 | 脏页占系统内存比例上限 | 写密集: 40-50 |
dirty_background_ratio |
10 | 后台刷脏页触发比例 | 写密集: 20-30 |
overcommit_memory |
0 | 内存超分配策略 | DB: 2(严格审计) |
compact_memory |
- | 触发内存规整 | 手动调试时用 |
8.2 推荐配置模板
通用文件服务器:
# /etc/sysctl.d/99-memory.conf
vm.swappiness = 10
vm.vfs_cache_pressure = 50
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.overcommit_memory = 0
vm.min_free_kbytes = 2097152 # 2GB on 64GB machine
高性能数据库(PostgreSQL / MySQL):
vm.swappiness = 1
vm.overcommit_memory = 2
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
vm.nr_hugepages = 2048 # 2MB huge pages
# 禁用 THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
九、监控与问题排查
9.1 关键监控指标
# 实时查看页面回收活动
sar -B 1 5
# 输出列:
# pgpgin/s, pgpgout/s, fault/s, majflt/s,
# pgfree/s, pgscank/s (kswapd扫描), pgscand/s (直接回收扫描),
# pgsteal/s (已回收页面)
# 查看 swap 使用详情
swapon --show
cat /proc/swaps
# 查看内存碎片情况
cat /proc/buddyinfo
cat /proc/pagetypeinfo
# 查看 zswap 统计
cat /sys/kernel/debug/zswap/*
9.2 排查流程
内存压力排查决策树:
应用延迟飙升?
│
├── 是 ──→ sar -B -- pgscand/s 高?
│ │
│ ├─Yes─→ Direct Reclaim 频繁,需加大 min_free_kbytes
│ │ 或扩容内存
│ │
│ └─No──→ swap 使用增长?
│ │
│ ├─Yes─→ 匿名页换出过多,检查 swappiness
│ │ 考虑增加 RAM 或启用 zram
│ │
│ └──No──→ 检查文件缓存压力
│ vfs_cache_pressure 是否合适?
│
└── ──→ 检查是否有进程内存泄漏
查看 /proc/<pid>/smaps 分析内存映射
9.3 使用 eBPF/BCC 监控回收延迟
# reclaimlat.py - 监控页面回收延迟 (基于 eBPF)
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
BPF_HISTOGRAM(reclaim_latency, u64);
int kretprobe__try_to_free_pages(struct pt_regs *ctx) {
u64 latency = bpf_ktime_get_ns() - start_time;
reclaim_latency.increment(bpf_log2l(latency / 1000)); // μs
return 0;
}
"""
b = BPF(text=prog)
reclaim_latency = b["reclaim_latency"]
reclaim_latency.print_log2_hist("reclaim latency (us)")
十、前沿技术演进
10.1 DAMON:数据访问监控框架
DAMON(Data Access MONitor)是 Linux 内核 5.15+ 引入的现代内存访问监控框架,可以:
# 查看 DAMON 是否启用
grep CONFIG_DAMON /boot/config-$(uname -r)
# 使用 damo 工具监控某进程的访问模式
damo record $(pidof my_app) &
sleep 30
damo report heats # 生成内存访问热度图
10.2 MGLRU:多代 LRU 算法
MGLRU(Multi-Generational LRU)是 Google 贡献的下一代 LRU 算法,引入页面"代龄"概念,替代简单的 Active/Inactive 二分法,更精确地判断页面的冷热状态,减少误回收。
# 检查当前内核是否支持 MGLRU
cat /sys/kernel/mm/lru_gen/enabled
# 启用 MGLRU(需要内核支持)
echo 7 > /sys/kernel/mm/lru_gen/enabled
总结
Linux 内存回收与交换子系统是一个精密的协作体系:
调优的核心思路不是追求"零 swap 使用",而是理解业务特性后,找到回收延迟、I/O 压力和内存利用率之间的最优平衡点。没有放之四海而皆准的配置,只有理解原理后针对场景的精准调优。
延伸阅读:本文聚焦匿名页回收与交换机制。关于 Slub 分配器、页表管理与 HugePage 的实现细节,可参考本系列之前的《Linux 内核 Slub 分配器深度分析与调优实战》和《KVM 虚拟化:VMX 与 vCPU 深度工程实战》。

发表评论 取消回复