上一篇把页回收的整体机制讲清楚了:水位线决定何时回收、kswapd 与 direct reclaim 决定谁来回收。但回收路径里对匿名页有一个前提条件——必须有地方可以把数据写出去。这个"地方"就是 swap。

理解 swap 的关键判断:

swap 是匿名页在内存压力下的 backing store;它不是系统失败的标志,而是把物理内存压力转成更慢的外部存储访问。

问题从哪里来

文件页天然有 backing store:磁盘上的原始文件。干净文件页可以直接丢弃,下次访问时从文件重新读入。脏文件页先写回再丢弃。无论哪种情况,数据都有归处。

匿名页没有这种归处。一块 malloc 出来的内存、一个栈帧、一段 mmap(MAP_ANONYMOUS) 区域——它们的内容只存在于物理内存中,没有对应文件。如果要回收这些页面,必须先把内容保存到某个外部存储,等将来再需要时读回来。

没有 swap 时,匿名页完全不可回收。内存压力一旦超过文件页能释放的量,系统直接到 OOM。这就是为什么纯粹"关掉 swap"在内存紧张的工作负载下会导致进程被杀——不是 swap 有害,而是关掉 swap 切断了匿名页唯一的回收通道。

swap 就是为匿名页提供一个"临时文件 backing"的机制。页面被写到 swap 设备(或文件),物理页释放;将来进程再访问该虚拟地址时,触发缺页异常把数据从 swap 读回新物理页。

swap 的生命周期

一个匿名页从分配到被 swap out 再 swap in 的完整路径:

1
2
3
4
5
6
7
8
9
10
11
malloc/mmap -> page fault -> allocate physical page (anon)
-> memory pressure -> reclaim selects this page
-> write page to swap slot (swap out)
-> free physical page
-> ...time passes...
-> process accesses the virtual address again
-> page fault: PTE contains swap entry (not PFN)
-> read page from swap slot (swap in)
-> allocate new physical page, fill from swap data
-> update PTE to point to new physical page
-> process resumes

几个关键点:

第一,swap out 时 PTE 不是被清空,而是被改写成 swap entry。swap entry 编码了 swap 设备号(swap type)和设备内偏移(swap offset)。硬件 MMU 看到这个 PTE 的 present 位为 0,下次访问触发缺页异常。

第二,缺页处理路径检查 PTE 内容:如果是全零则分配零页或触发 VMA 的 fault handler;如果包含 swap entry 则走 swap-in 路径——从 swap 设备读取数据到新物理页。

第三,swap out 不是原子的。一页可能被多个进程映射(fork 后共享的匿名页)。回收时需要通过反向映射找到所有映射方,逐一把 PTE 改写成 swap entry。

swap cache

swap cache 是理解 swap 机制时容易忽略的中间层。

当一页被从 swap 读入内存时,它同时存在于物理内存和 swap 设备上(swap 设备上的副本没有立即释放)。此时该页面处于 swap cache 中——既在 page cache 管理下(通过 address_space 的 xarray 索引),又保持着 swap slot 的引用。

swap cache 存在的理由:

第一,避免重复读取。如果多个进程共享同一个被 swap out 的匿名页(通过相同的 swap entry),第一个 fault 的进程把页面读入 swap cache 后,后续进程可以直接从 cache 中获取,不需要再读一次 swap 设备。

第二,延迟释放 swap slot。只有当页面被修改(变脏)且不再需要 swap 备份时,swap slot 才会被释放。如果页面读入后没有被修改,下次回收时可以直接丢弃物理页(因为 swap 上的副本还在),不需要再次写出。

第三,处理竞态。多个 CPU 可能同时对同一个 swap entry 触发 fault。swap cache 作为同步点,确保只有一次实际的 I/O 读取。

1
2
3
4
5
6
7
8
9
10
swap out:
anon page -> add to swap cache -> write to swap device
-> remove all PTEs (replace with swap entry)
-> remove from swap cache -> free physical page

swap in:
fault on swap entry -> check swap cache (already loading?)
-> if not: allocate page, read from swap, add to swap cache
-> map page into process page table
-> (later) remove from swap cache when swap slot freed

swap 设备与 swap 文件

Linux 支持两种 swap 后端:

类型 创建方式 特点
swap 分区 mkswap /dev/sdX 直接块设备访问,略快
swap 文件 fallocate + mkswap + swapon 不需要专用分区,灵活

多个 swap 区域可以同时激活,内核通过优先级选择写入哪个。高优先级的先用满,低优先级的作为溢出。

每个 swap 区域内部用一个 bitmap 追踪哪些 slot 已用、哪些空闲。分配 slot 时尝试连续分配以利用磁盘顺序 I/O。swap 区域的大小决定了系统能 swap out 的匿名页总量上限。

zswap:压缩层

zswap 在 swap 路径上插入一个压缩缓存层。页面被 swap out 时,不直接写到磁盘,而是先压缩后存在内存的压缩池中。

1
2
3
4
5
6
7
8
9
10
swap out path:
page -> zswap compress -> store in RAM (compressed)
(no disk I/O unless pool full)

swap in path:
fault -> zswap decompress -> page ready
(no disk I/O if page still in zswap)

pool full:
zswap evicts oldest entries -> write to real swap device

zswap 的核心权衡:用 CPU 周期(压缩/解压)换取减少的磁盘 I/O。对于压缩率好的数据(典型的应用内存经常有大量零或重复模式),一页 4KB 可能压缩到几百字节,等于用少量内存存储了更多的"虚拟 swap 内容"。

zswap 使用 xarray 索引压缩数据,每个 swap 类型一棵,swap offset 作为查找键。分配器使用 zsmalloc,返回的是 handle 而非直接地址——访问前需要 map。对于全零页或相同值填充页,zswap 直接记录 pattern 而不实际压缩,进一步节省空间。

当压缩池达到 max_pool_percent 上限时,zswap 按 LRU 策略把最冷的压缩页 writeback 到真正的 swap 设备。accept_threshold_percent 参数在池满时拒绝新页面进入,防止 thrashing。

模式提炼:分层缓存吸收压力

1
anonymous state -> compressed cache -> external backing -> fault-driven restore
层次 延迟 容量 触发条件
物理内存(匿名页) ns 级 受限于 RAM 正常访问
zswap 压缩池 μs 级(解压) RAM 的一部分 × 压缩率 swap out 时先入此层
swap 设备 ms 级(磁盘 I/O) swap 分区/文件大小 zswap 池满时溢出

这种"热→压缩→冷存储"的分层在系统设计中反复出现:CPU cache hierarchy(L1→L2→L3→DRAM)、分布式存储的 hot/warm/cold tiering、数据库的 buffer pool → compressed page → disk。核心思想是用少量快速资源覆盖大部分访问,大量慢速资源兜底长尾。

一个最小实验:观察 swap 活动

这个实验通过在内存受限环境中分配超量匿名页,观察 swap out/in 的发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void read_swap_stats(const char *label) {
FILE *f = fopen("/proc/vmstat", "r");
if (!f) return;
printf("--- %s ---\n", label);
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "pswpin", 6) == 0 ||
strncmp(line, "pswpout", 7) == 0 ||
strncmp(line, "swap_ra", 7) == 0 ||
strncmp(line, "nr_swapcached", 13) == 0) {
printf(" %s", line);
}
}
fclose(f);
}

int main(void) {
read_swap_stats("initial");

/* 分配 256MB 匿名内存并全写 */
size_t size = 256UL * 1024 * 1024;
char *buf = malloc(size);
if (!buf) { perror("malloc"); return 1; }

long page_size = sysconf(_SC_PAGESIZE);
for (size_t off = 0; off < size; off += (size_t)page_size) {
buf[off] = (char)(off & 0xFF);
}
read_swap_stats("after writing 256MB");

/* 只访问前 32MB,让后 224MB 变冷 */
for (int round = 0; round < 30; round++) {
volatile char sink = 0;
size_t hot = 32UL * 1024 * 1024;
for (size_t off = 0; off < hot; off += (size_t)page_size) {
sink += buf[off];
}
(void)sink;
usleep(100000);
}
read_swap_stats("after 30 rounds on hot 32MB");

/* 重新访问冷区,触发 swap in */
volatile char sink2 = 0;
for (size_t off = 128UL * 1024 * 1024; off < size; off += (size_t)page_size) {
sink2 += buf[off];
}
(void)sink2;
read_swap_stats("after re-reading cold region");

free(buf);
return 0;
}

编译与运行(需要启用 swap 的内存受限环境):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cc -Wall -Wextra -O2 swap_demo.c -o swap_demo

# 方法一:使用 cgroup v2 限制内存(推荐)
mkdir -p /sys/fs/cgroup/swap_test
echo 128M > /sys/fs/cgroup/swap_test/memory.max
echo max > /sys/fs/cgroup/swap_test/memory.swap.max
echo $$ > /sys/fs/cgroup/swap_test/cgroup.procs
./swap_demo

# 方法二:使用临时 swap 文件(如果机器没有 swap)
dd if=/dev/zero of=/tmp/swapfile bs=1M count=512
chmod 600 /tmp/swapfile
mkswap /tmp/swapfile
sudo swapon /tmp/swapfile
# ... 运行实验 ...
sudo swapoff /tmp/swapfile
rm /tmp/swapfile

完成实验后清理:

1
2
3
4
rm -f swap_demo swap_demo.c
# 如果创建了 cgroup:
# echo $$ > /sys/fs/cgroup/cgroup.procs
# rmdir /sys/fs/cgroup/swap_test

读取实验结果

在 128MB 内存限制 + swap 可用的环境下预期看到的行为:

第一阶段(写入 256MB):分配超过 128MB 限制后,cgroup 的回收机制开始工作。由于匿名页无法丢弃,只能 swap out。pswpout 开始增长。在写完 256MB 后,至少有 128MB 的数据被写到 swap 设备。

第二阶段(反复访问前 32MB):热区的 32MB 被保护在内存中。冷区的页面如果还在内存中,逐渐被 swap out 给热区腾空间。pswpout 可能继续增长。

第三阶段(重新访问冷区):访问已被 swap out 的页面触发缺页异常,走 swap-in 路径。pswpin 显著增长。同时 swap_ra(swap readahead)可能非零——内核猜测接下来还会访问相邻的 swap slot,提前读入。

关键观察:

  • pswpout 反映被写到 swap 的页面数。值越大说明匿名页回收压力越大。
  • pswpin 反映从 swap 读回的页面数。高 pswpin 意味着之前被 swap out 的页面又被需要了——working set 超过物理内存。
  • nr_swapcached 反映当前在 swap cache 中的页面数(同时存在于内存和 swap 设备上)。
  • 如果没有 swap(或 memory.swap.max=0),第一阶段写入超过 128MB 时会直接触发 cgroup OOM,进程被杀。

模式提炼:有退路才能优雅降级

1
2
with swap:    pressure -> swap out cold pages -> slower but alive
without swap: pressure -> file pages exhausted -> OOM kill

swap 的存在不是让系统变慢的原因,而是让系统在内存压力下有一条退路。这和分布式系统的思路一致:circuit breaker 让请求降级到慢路径,而不是直接失败;数据库的 spill-to-disk 让大查询变慢但不 OOM。

vm.swappiness

vm.swappiness 参数控制内核在回收时对匿名页 vs 文件页的偏好。

行为
0 几乎不 swap 匿名页(除非文件页已无可回收)
60(默认) 平衡回收文件页和匿名页
100 同等对待匿名页和文件页
200(cgroup v2) 更积极地 swap

这个参数不是"swap 使用量的百分比",也不是"swap 开始的阈值"。它是回收决策中匿名页权重的调节旋钮。低 swappiness 意味着内核尽量留匿名页在内存中,优先回收文件页;高 swappiness 意味着匿名页和文件页被更平等地对待。

在 cgroup v2 中,memory.swap.max 控制该 cgroup 可用的 swap 总量;swappiness 可以通过 memory.swappiness per-cgroup 设置,但这个接口是否存在取决于内核版本。

核心代码:swap entry 编码

PTE 里存储 swap 位置的编码方式(include/linux/swapops.h,精简):

1
2
3
4
5
6
7
8
9
10
11
/* swap entry 编码进 PTE(非 present 状态) */
static inline swp_entry_t pte_to_swp_entry(pte_t pte) { ... }
static inline pte_t swp_entry_to_pte(swp_entry_t entry) { ... }

/* swap entry = (type, offset) */
static inline unsigned swp_type(swp_entry_t entry) {
return (entry.val >> SWP_TYPE_SHIFT) & SWP_TYPE_MASK;
}
static inline pgoff_t swp_offset(swp_entry_t entry) {
return entry.val & SWP_OFFSET_MASK;
}

type 标识哪个 swap 设备,offset 标识设备内的 slot 位置。PTE 的 present 位为 0 时,剩余位被解释为 swap entry 而非物理地址。

源码锚点

入口 读它的目的
mm/swapfile.c swap 区域管理、slot 分配与释放、swapon/swapoff 系统调用
mm/swap_state.c swap cache 的 add/delete/lookup、swap_address_space
mm/page_io.c swap_readpage()swap_writepage():实际 I/O 路径
mm/memory.c do_swap_page():swap-in 的缺页处理入口
mm/zswap.c zswap 压缩/解压、pool 管理、writeback
include/linux/swap.h swap entry 编码、swap_info_struct 定义

do_swap_page() 时可以带着一个问题:从 swap entry 到页面可用,中间经过哪些锁和等待点?这些等待点直接决定 swap-in 的延迟。

研究生迁移表

Linux 概念 一般系统设计
swap 设备 溢出到慢速层(tiered storage spill)
swap entry(PTE 中编码) 指向外部存储的间接引用(remote pointer)
swap cache 读缓存 + 去重同步层
zswap 压缩池 内存中的压缩 tier(compressed cache)
swap readahead 预取策略(prefetch on sequential access)
vm.swappiness 多类资源回收的权重配置
swap slot 分配 块设备空间管理(bitmap allocator)

swap 的整体设计模式——“主存储满时溢出到外部,按需读回”——与数据库 buffer pool eviction to disk 直接对应。

常见误解

第一个误解是"有 swap 活动就意味着系统有问题"。少量 swap 活动是正常的——把长时间不用的匿名页(比如初始化后不再访问的配置数据)swap out,腾出内存给活跃的文件页 cache,整体性能反而更好。

第二个误解是把 vm.swappiness=0 理解为"禁止 swap"。即使 swappiness 为 0,当文件页回收无法满足需求时,内核仍然会 swap 匿名页。真正禁止 swap 需要 swapoff -amemory.swap.max=0

第三个误解是认为 swap 分区一定比 swap 文件快。在现代文件系统(ext4、XFS)上,swap 文件的性能与 swap 分区接近。主要差异在于 swap 文件经过文件系统层有额外的 metadata 路径,但对 SSD 上的随机访问模式影响很小。

第四个误解是认为 zswap 能无限扩展可用内存。zswap 的压缩池本身占用物理内存。如果数据压缩率差(已经是随机数据),zswap 反而浪费内存(压缩后可能比原始数据还大加上 metadata 开销)。zswap 对零页和重复模式数据效果最好。另一个相关的常见混淆:swap-in 后 swap slot 并不立即释放——页面读回后仍保留在 swap cache 中,只有被修改或显式释放时 slot 才归还(这使得未修改页面再次 swap out 时可跳过写出)。

练习

第一,在一台有 swap 的 Linux 机器上运行文中实验(使用 cgroup 限制内存到 128MB),记录四个阶段的 pswpinpswpoutnr_swapcached 变化。改变热区大小(16MB、64MB、128MB),观察哪个设置下 pswpin 最高。

第二,比较有 swap 和无 swap 两种配置下分配超量匿名内存的行为差异。无 swap 时观察 dmesg 中的 OOM 信息,记录从分配开始到进程被杀的时间。

第三,在一台启用了 zswap 的机器上(检查 /sys/module/zswap/parameters/enabled),观察 /sys/kernel/debug/zswap/ 下的统计信息。分配大量可压缩数据(全零页)和不可压缩数据(随机字节),比较 stored_pagespool_total_size 的关系。

第四,阅读 mm/memory.cdo_swap_page() 的实现。画出从检测到 swap entry 开始、到页面就绪并恢复用户态执行的完整调用链。标出可能的睡眠点。

第五,用 swapon --show 查看当前系统的 swap 配置。创建一个 256MB 的 swap 文件,用 swapon -p 10 设置为高优先级。在已有 swap 分区的基础上观察新 swap 文件是否被优先使用(通过 /proc/swaps 的 Used 列)。

系列导航

参考资料