深入理解linux内核--文件页高速缓存,页框回收,性能优化

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 主动回收接口
相关推荐
badboy1212 小时前
Xilinx Vitis Core Development Kit 2025.2.1_Win+Linux
linux·fpga开发·xilinx
那年窗外下的雪.2 小时前
Linux 学习笔记
linux·笔记·学习
Jay Kay2 小时前
BAGEL 训练性能优化报告
性能优化
小张同学a.3 小时前
MooseFS 分布式存储实战指南:部署、运维与 Pacemaker+SBD 高可用落地
linux·运维·分布式·pacemaker·moosefs·sbd
精进之道3 小时前
拆开一个 .rpm 文件,里面到底装了什么?
linux
wuminyu3 小时前
JDK21 FFM异步读取MSG_ZEROCOPY错误队列揭秘
java·linux·c语言·jvm·c++
Eloudy3 小时前
sunshine - ubuntu 22.04, moonlight - mac OS26 远程桌面
linux·运维·ubuntu
苍狗T3 小时前
K8s 集群实战:基于 Harbor 私有仓库 + cri‑dockerd 完整部署
linux·运维·云原生·容器·kubernetes
石小千3 小时前
X86下载 ARM 架构的包
linux·运维