1.文件页高速缓存机制(Page Cache)
1.1.核心概念
Page Cache 是 Linux 内核在 RAM 中缓存文件数据的机制。当应用程序读取文件时,内核将数据从磁盘读入内存页,后续读取直接从内存返回,避免重复磁盘 I/O。
速度差距决定了缓存的必要性:
| 存储层级 | 延迟 | 带宽 |
|---|---|---|
| RAM | ~100ns | ~100 GB/s |
| NVMe SSD | ~100µs | ~7 GB/s |
| HDD | ~10ms | ~200 MB/s |
1.2.核心数据结构
1.2.1.struct address_space
cpp
struct address_space {
struct inode *host; /* 所属 inode */
struct xarray i_pages; /* 缓存页索引(XArray) */
unsigned long nrpages; /* 缓存页数量 */
const struct address_space_operations *a_ops; /* 操作回调 */
};
页通过文件偏移量索引到 XArray 中,实现 O(log n) 级别的快速查找。
1.2.2.XArray(v4.20+,替代 Radix Tree)
XArray 是页缓存的索引结构,相比旧的 Radix Tree 提供更清晰的 API 和相同的性能。
1.3.读文件流程
c
read(fd, buf, 4096)
│
▼
在 XArray 中查找页?
├── Yes ──► 直接拷贝到用户缓冲区(~100ns)
│
└── No ──► 分配新页
│
▼
从磁盘读取到页
│
▼
加入 XArray + LRU
│
▼
拷贝到用户缓冲区
1.4.写文件与 Writeback
Linux 采用 Write-Back(回写) 策略:数据先写入 Page Cache,标记为 Dirty,由后台线程异步刷盘。
cpp
write(fd, buf, 4096)
│
▼
查找/分配缓存页
│
▼
从用户缓冲区拷贝到页
│
▼
标记页为 Dirty(SetPageDirty)
│
▼
立即返回用户态(write "完成")
│
▼(稍后,异步)
pdflush / kworker 刷写到磁盘
Dirty Page 生命周期:Clean → Dirty → Writeback → Clean
1.5.Readahead(预读)
内核检测顺序访问模式,提前预取后续页:
c
// 默认预读窗口大小(KB)
cat /sys/block/sda/queue/read_ahead_kb
# 128(默认值)
// 顺序读取时,内核自动扩展窗口
// 首次读 page 0 → 预取 page 1,2,3,4...
// 应用读 page 1 时,已命中缓存
1.6.mmap vs read()
| 方式 | 机制 | 适用场景 |
|---|---|---|
read() |
从缓存拷贝到用户缓冲区 | 顺序读取、简单场景 |
mmap() |
将缓存页直接映射到进程地址空间 | 随机访问、大文件 |
mmap(MAP_SHARED) 实现真正的零拷贝------页缓存页直接映射到进程虚拟地址空间。此时修改会直接影响到磁盘文件。
mmap(MAP_PRIVATE) 读操作零拷贝,写操作触发 COW。此时修改会引发写时复制,针对内存复制页修改,对磁盘文件无影响。
2.页框回收机制(Page Frame Reclaiming)
2.1.PFRA 概述
页框回收算法(Page Frame Reclaiming Algorithm, PFRA)的目标是在内存紧张时选择合适的页框回收。Linux 将页分为四类:
| 类型 | 说明 | 回收方式 |
|---|---|---|
| Unreclaimable | 内核数据结构、pinned 页 | 不可回收 |
| Swappable | 匿名页(堆、栈) | 写入 swap 分区 |
| Syncable | 脏文件页 | 写回磁盘后回收 |
| Discardable | 干净文件页 | 直接丢弃(磁盘有副本) |
2.2.回收触发条件
回收由两类机制触发:
- kswapd(后台守护线程):当 zone 的 pages_free < pages_low 时唤醒,异步、预防性回收
- Direct Reclaim:__alloc_pages() 分配失败时,当前进程直接调用 try_to_free_pages(),同步、会阻塞应用
2.3.传统 LRU 机制
Linux 将页按类型和活跃度组织为 4 个 LRU 链表:
c
┌─────────────────┐
│ Active File │ ← 最近访问的文件页
├─────────────────┤
│ Inactive File │ ← 回收候选文件页
├─────────────────┤
│ Active Anon │ ← 最近访问的匿名页
├─────────────────┤
│ Inactive Anon │ ← 回收候选匿名页(→ swap)
└─────────────────┘
2.4 MGLRU(Multi-Gen LRU)------ 现代替代方案
传统 LRU 的痛点:
- 扫描精度低(大量页被扫描但无法回收)
- CPU 开销高(频繁遍历链表)
- 内存压力下容易抖动(thrashing)
MGLRU 将页按"代"(Generation)组织,通常 0~3 代:
| 代 | 含义 |
|---|---|
| Gen 3 | 最新访问的页 |
| Gen 2 | 近期访问的页 |
| Gen 1 | 较早访问的页 |
| Gen 0 | 最老的页 → 优先回收 |
优势:
- 扫描精度更高:老代的页更可能可回收
- CPU 开销更低:通过页表遍历(page table walk)检测访问位,而非遍历整个链表
- 内置 PID 控制器:动态平衡扫描开销与回收收益
- 减少抖动:更准确地识别工作集
2.5 核心回收流程:shrink_lruvec()
cpp
// mm/vmscan.c
static void shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
{
// 1. MGLRU 路径
if (lru_gen_enabled() && !root_reclaim(sc)) {
lru_gen_shrink_lruvec(lruvec, sc);
return;
}
// 2. 传统 LRU 路径
get_scan_count(lruvec, sc, nr); // 计算各 LRU 扫描数量
while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
nr[LRU_INACTIVE_FILE]) {
for_each_evictable_lru(lru) {
nr_reclaimed += shrink_list(lru, nr_to_scan, lruvec, sc);
}
cond_resched();
}
}
回收优先级(从高到低):
- Inactive File(干净文件页直接丢弃,成本最低)
- Active File(需先降级到 Inactive)
- Inactive Anon(需 swap out)
- Active Anon(最后手段)
2.6.回收页的处理
在 shrink_folio_list() / shrink_page_list() 中:
| 页类型 | 处理方式 |
|---|---|
| 干净文件页 | 直接丢弃,PTE Present 位清 0 |
| 脏文件页 | 先写回磁盘,再丢弃 |
| 匿名页 | 写入 swap 分区,PTE 改为 swap 标识符 |
| 被引用的页 | 无法回收,放回 LRU |
3.性能优化
3.1.Page Cache 层面优化
3.1.1. Readahead 调优
c
# 查看当前预读大小
cat /sys/block/sda/queue/read_ahead_kb
# 128(默认值,KB)
# 顺序大文件读取场景(如日志分析、视频流)可增大
echo 4096 > /sys/block/sda/queue/read_ahead_kb
3.1.2.使用 posix_fadvise / madvise
c
// 告知内核访问模式
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); // 顺序访问
posix_fadvise(fd, 0, 0, POSIX_FADV_WILLNEED); // 预加载到缓存
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); // 使用完后释放缓存
// 内存建议
madvise(ptr, len, MADV_SEQUENTIAL); // 顺序访问,允许激进预读
madvise(ptr, len, MADV_WILLNEED); // 预填充
madvise(ptr, len, MADV_DONTNEED); // 释放缓存
3.2.Writeback 层面优化
控制脏页回写的关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
vm.dirty_ratio |
40% | 脏页占总内存比例达到此值时,阻塞写操作强制回写 |
vm.dirty_background_ratio |
10% | 脏页达到此值时,后台线程开始回写 |
vm.dirty_expire_centisecs |
3000 (30s) | 脏页在缓存中最长存活时间 |
vm.dirty_writeback_centisecs |
500 (5s) | 回写线程唤醒间隔 |
调优建议:
- 高吞吐写入(如日志系统):提高 dirty_ratio 到 60~80,减少刷盘频率
- 低延迟要求(如数据库):降低 dirty_background_ratio 到 5,加快回写
- 数据安全优先:降低 dirty_expire_centisecs 到 1000 (10s),减少崩溃丢失数据量
c
# 高吞吐场景示例
sysctl -w vm.dirty_ratio=80
sysctl -w vm.dirty_background_ratio=10
sysctl -w vm.dirty_expire_centisecs=6000 # 60s
3.3.Reclaim 层面优化
3.3.1.Swappiness
c
# 控制匿名页 vs 文件页的回收倾向(0-100)
cat /proc/sys/vm/swappiness
# 60(默认值)
# 数据库/缓存服务器:降低 swappiness,优先保留匿名页
sysctl -w vm.swappiness=10
# 桌面环境:保持默认值或略高
3.3.2.启用 MGLRU
c
# 检查是否启用
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 表示启用(file + anon + 页表遍历)
# 动态启用
echo y > /sys/kernel/mm/lru_gen/enabled
3.3.3.水位线调优
c
# 最小保留内存(KB),用于紧急回收
cat /proc/sys/vm/min_free_kbytes
# 默认值通常较小,高负载服务器可适当增大
# 水位线缩放因子
cat /proc/sys/vm/watermark_scale_factor
# 控制 kswapd 的激进程度
3.4.应用层优化
| 技术 | 适用场景 | 效果 |
|---|---|---|
mmap(MAP_SHARED) |
大文件随机读 | 零拷贝,减少内核态拷贝 |
O_DIRECT |
数据库(MySQL/PostgreSQL) | 绕过 Page Cache,避免双缓冲 |
sync_file_range() |
大文件部分同步 | 精确控制刷盘范围,避免全局 sync |
fallocate() |
预分配文件空间 | 减少写时分配开销 |
3.5.监控与诊断
c
# 1. 查看缓存整体状态
cat /proc/meminfo | grep -E "Cached|Buffers|Dirty|Writeback|AnonPages"
# Cached: 8765432 kB (Page Cache)
# Dirty: 1234 kB (待回写脏页)
# Writeback: 567 kB (正在回写)
# AnonPages: 3456789 kB (匿名页)
# 2. 缺页与交换统计
cat /proc/vmstat | grep -E "pgpgin|pgpgout|pswpin|pswpout|pgscan|pgsteal"
# pgpgin/pgpgout: 磁盘读写页数
# pswpin/pswpout: swap 换入/换出
# pgscan/pgsteal: 扫描/回收页数
# 3. 查看单个文件的缓存状态
vmtouch -v /path/to/large_file
# Files: 1
# Directories: 0
# Resident Pages: 50000/50000 195MB/195MB 100%
# Elapsed: 0.1 seconds
# 4. ftrace 追踪页缓存事件
echo 1 > /sys/kernel/debug/tracing/events/filemap/mm_filemap_add_to_page_cache/enable
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_dirty_page/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 5. 查看 zone 水位线
cat /proc/zoneinfo | grep -E "Node|zone|pages free|pages min|pages low|pages high"
3.6.关键权衡总结
| 优化方向 | 收益 | 代价/风险 |
|---|---|---|
| 增大 dirty_ratio | 更高写吞吐 | 崩溃时更多数据丢失 |
| 启用 MGLRU | 更低 CPU 开销、更高回收精度 | 状态切换存在竞态(已修复) |
| 使用 O_DIRECT | 避免双缓冲、精确 I/O 控制 | 失去内核缓存加速、需应用自己管理缓存 |
| 增大 read_ahead_kb | 顺序读更快 | 随机读浪费带宽和缓存 |
| 降低 swappiness | 减少 swap 使用 | 文件页被过度回收,I/O 增加 |
生产环境最佳实践:
- 数据库服务器:swappiness=1~10 + O_DIRECT + 禁用 THP + 适当 dirty_ratio
- Web 服务器:保持默认 + 启用 MGLRU + 监控 Cached 和 Dirty
- 大数据/批处理:增大 read_ahead_kb + posix_fadvise(POSIX_FADV_SEQUENTIAL)
- 容器环境:配置 memcg memory.high / memory.max 限制,配合 cgroup v2 的 memory.reclaim 主动回收接口