Linux 的 Page Cache

1. 初识 Page Cache

1.1 什么是 Page Cache?

  • 定义:Linux 内核用于缓存文件数据的内存区域。

  • 单位 :以页(Page)为基本单位,通常一页为 4KB。

  • 位置:位于内核空间,与虚拟内存系统深度集成。

  • 共享性:支持跨进程共享。多个进程访问同一文件时,可共享同一份页缓存,避免重复读盘。

  • 核心思想:

    • 读文件:先查页缓存,命中则直接返回;未命中才读磁盘。

    • 写文件:先写页缓存,标记为脏页;之后由内核异步刷回磁盘。

1.2 为什么需要 Page Cache?

磁盘和内存速度差距巨大:

对比项 内存 磁盘
访问延迟 纳秒级 毫秒级
吞吐 GB/s 级别 MB/s 级别
差距 可达百万倍 / 千倍 ------

如果每次读写都直接访问磁盘,系统性能会极差。Page Cache 通过内存缓存文件数据,大幅减少磁盘 I/O。

1.3 核心作用

  1. 加速文件读取

    • 热点文件如配置文件、日志、数据库数据文件,一旦缓存,后续访问几乎零延迟。

    • 例:Nginx 静态资源第一次读盘后进入页缓存,后续大量请求直接命中内存。

  2. 合并写操作

    • 多次小写入先缓存在页缓存,后续合并为一次大块写入。

    • 减少磁盘 I/O 次数,提升吞吐,延长 SSD 寿命。

  3. 支持"零拷贝"优化

    • 如 sendfile() 可直接从页缓存传输到网络 socket,避免用户态缓冲区拷贝。

    • Kafka 等高吞吐系统常利用此特性。


2. Page Cache 工作机制

2.1 读路径:从磁盘到页缓存

用户程序调用 read() 时:

  1. 用户态陷入内核态。

  2. VFS 根据文件路径找到 inode,再根据 inode + 偏移量在页缓存中查找。

  3. 命中:

    • 直接通过 copy_to_user 将数据从页缓存拷贝到用户缓冲区。

    • 完全跳过磁盘 I/O。

  4. 未命中:

    • 触发缺页异常。

    • VFS 调用具体文件系统的 readpage / readpages。

    • 文件系统计算逻辑块地址 LBA,交给块设备层。

    • I/O 调度器优化排序,如 CFQ、Deadline 等。

    • 磁盘通过 DMA 将数据传入内存。

    • 数据填充到 struct page,加入页缓存。

    • 更新 address_space 的基数树 radix tree,建立"文件 inode + 页内偏移"到物理页的映射。

    • 最后拷贝到用户空间。

关键点:文件数据不会直接从磁盘读到用户空间,必须经过 Page Cache。

2.2 预读机制

  • 当程序读取某一部分数据时,内核预测后续可能继续读取相邻数据。

  • 因此不只加载当前 4KB,还会提前加载后续连续页。

  • 默认会根据文件类型和访问模式动态调整,常见预读大小为 16KB 或 32KB。

  • 作用:后续访问直接命中缓存,减少磁盘 I/O。

2.3 写路径与脏页

用户调用 write():

  1. 用户态陷入内核态。

  2. 内核检查文件描述符有效性。

  3. 通过 copy_from_user 将数据从用户空间拷贝到页缓存。

  4. 对应页面被标记为脏页,设置 struct page 的 PG_dirty 标志。

  5. 此时数据与磁盘不一致,但不会立即写盘。

  6. 后续由内核在合适时机异步回写。

示例:

c

复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main() {
    const char* message = "Hello, Linux Page Cache!";
    int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    ssize_t bytes_written = write(fd, message, strlen(message));
    if (bytes_written < 0) {
        perror("write");
    } else {
        printf("Wrote %zd bytes to file.\n", bytes_written);
    }

    close(fd);
    return 0;
}

写入页缓存后,数据不会立刻落盘。若系统崩溃,未回写的脏页可能丢失。

2.4 脏页刷新机制

内核提供三种可靠刷新机制:

  1. 定时刷新

    • 内核 flusher / writeback 线程周期性扫描脏页。

    • 达到时间阈值或脏页比例后,分批写入磁盘。

  2. 主动触发刷新

    • 用户调用 fsync、fdatasync、sync。

    • 强制立即写入并等待 I/O 完成。

    • 适合数据库、日志、关键文件保存。

  3. 内存不足时强制刷新

    • 内存紧张时,内核回收页缓存。

    • 优先释放干净页。

    • 干净页不足时,强制回写脏页,再释放内存。


3. 脏页回写机制

3.1 回写触发条件

触发方式 说明
周期性回写 每个回写设备有定时器 wb_timer,由 vm.dirty_writeback_centisecs 控制,单位 0.01 秒
比例回写 脏页达到 vm.dirty_background_ratio 启动后台异步回写;达到 vm.dirty_ratio 阻塞新写
显式同步 sync、fsync、fdatasync
内存压力触发 kswapd 等回收内存时,遇脏页先回写再释放

3.2 回写过程

  1. 后台 writeback 线程执行回写。

    • Linux 2.6.32+ 已废弃 pdflush。
  2. 从脏页链表获取页面。

  3. 每个脏页关联 address_space,包含文件索引、磁盘地址等信息。

  4. 调用文件系统的 writepages。

  5. 通过块设备层提交 I/O,如 ext4 计算逻辑块地址,使用 submit_bio 下发。

  6. 数据写入磁盘后,清除 PG_dirty 标记,页面变为干净页,可回收复用。

伪代码:

复制代码
struct page *page = get_dirty_page();
if (PageDirty(page)) {
    writepages(page->mapping);  // 写入磁盘
    ClearPageDirty(page);       // 清除脏标记
}

3.3 相关内核参数

参数 作用
vm.dirty_ratio 最大脏页比例,达到后阻塞新写,默认约 20%
vm.dirty_background_ratio 后台异步回写触发比例,默认约 10%
vm.dirty_expire_centisecs 脏页最大存活时间,超时强制回写
vm.dirty_writeback_centisecs 回写线程周期性唤醒间隔,单位 0.01 秒

查看与设置:

复制代码
sysctl vm.dirty_ratio
sysctl vm.dirty_background_ratio
sysctl -w vm.dirty_writeback_centisecs=500   # 5 秒

4. 实际应用场景

4.1 静态文件服务

  • Nginx、Apache 部署 HTML、CSS、JS、图片等。

  • 第一次访问从磁盘读取并加载到页缓存。

  • 后续成千上万次访问直接命中页缓存。

  • 这是静态文件服务支撑高并发的关键。

4.2 数据库系统

  • MySQL、PostgreSQL 的表数据、索引本质都是磁盘文件。

  • 查询优先从页缓存读取,减少磁盘 I/O。

  • 数据库自身缓存如 InnoDB Buffer Pool 与页缓存配合。

  • 即使数据库缓存未命中,若页缓存命中,也能减少磁盘开销。

4.3 日志写入场景

  • Java logback、Python logging 默认延迟写入,依赖页缓存。

  • 写入先进入页缓存,内核后台批量刷新。

  • 风险:程序崩溃时,未刷盘脏页可能丢失。

  • 核心日志应使用 fsync 主动刷新,平衡性能与安全。


5. Page Cache 调优方法

5.1 调整页缓存大小与内存置换

vm.pagecache_limit_mb
  • 硬性限制页缓存最大占用,单位 MB。

  • 适用于内存紧张、核心应用需要保留内存的场景。

  • 例:16GB 内存服务器,可为应用预留 12GB,将页缓存限制为 4096MB。

    sysctl vm.pagecache_limit_mb
    sysctl -w vm.pagecache_limit_mb=4096
    echo "vm.pagecache_limit_mb=4096" >> /etc/sysctl.conf
    sysctl -p

vm.swappiness

控制内存不足时,内核优先回收页缓存还是置换 Swap。

取值 倾向 适用
0~20 保留页缓存,优先置换 Swap IO 密集型,如 Nginx、日志读取
60~100 回收页缓存,保留应用内存 计算密集型
生产通用 IO 密集 10~20,通用 30~50 ------

禁止设置为 0,极端情况下可能禁用 Swap,引发内存溢出风险。

复制代码
sysctl vm.swappiness
sysctl -w vm.swappiness=10

5.2 优化预读大小

查看:

复制代码
cat /sys/block/sda/queue/read_ahead_kb

设置:

复制代码
# 大文件:日志、视频、备份
echo 512 > /sys/block/sda/queue/read_ahead_kb

# 小文件:配置、短文本
echo 64 > /sys/block/sda/queue/read_ahead_kb
  • 设备级 read_ahead_kb 优先级高于全局 vm.readahead_size。

  • 默认通常 128KB。

  • 大文件顺序读:可调高到 256KB~1024KB。

  • 小文件随机读:建议 64KB 及以下,避免缓存污染。

  • 修改后实时生效,无需重启。

5.3 合理选择文件 IO 接口

IO 方式 特点 适用
标准缓存 IO read/write 完全依赖页缓存,性能最优 高频读、小文件、非核心数据
直接 IO O_DIRECT 跳过页缓存,应用缓冲区与磁盘直接传输 数据库、大文件备份、日志落盘
同步落盘 fsync/fdatasync 配合标准 IO,主动刷脏页 交易、订单、数据库事务
同步 IO O_SYNC 写入同步落盘,实时性最强,性能损耗大 极高安全性场景

示例:

复制代码
// 标准缓存 IO
int fd = open("app.log", O_RDONLY);
read(fd, buf, 1024);
close(fd);

// 直接 IO:跳过页缓存
int fd = open("backup.db", O_RDWR | O_DIRECT);

// 写入后强制落盘
write(fd, data, len);
fsync(fd);

O_DIRECT 需满足内存对齐、块大小对齐,否则可能触发内核降级处理。

5.4 清理页缓存(应急运维)

需 root 权限,通过 /proc/sys/vm/drop_caches:

复制代码
echo 1 > /proc/sys/vm/drop_caches  # 仅清理页缓存
echo 2 > /proc/sys/vm/drop_caches  # 清理 dentry 和 inode 缓存
echo 3 > /proc/sys/vm/drop_caches  # 全面清理

注意:

  • 不会丢失磁盘数据,只清内存缓存。

  • 清理后后续读文件会重新从磁盘加载,可能产生大量磁盘 I/O。

  • 禁止生产环境频繁、定时执行。

  • 禁止写定时任务自动清理缓存。

5.5 脏页回写联动调优

  • 高写入业务:降低 vm.dirty_ratio 和 vm.dirty_background_ratio,避免大量脏页阻塞业务。

  • 高性能存储业务:调高 vm.dirty_writeback_centisecs,减少频繁回写带来的 I/O 抖动。

  • 组合调优:页缓存大小、预读、IO 接口、脏页机制一起配合,达到缓存命中率、内存利用率、磁盘 I/O 性能的平衡。


6. 速查表

主题 关键点
定义 内核缓存文件数据的内存区域,以 4KB 页为单位
读命中 直接 copy_to_user,跳过磁盘
读未命中 缺页异常 → 文件系统 readpage → 块设备 → DMA → 页缓存
预读 提前加载连续页,常见 16KB/32KB,可调 read_ahead_kb
写操作 先写页缓存,标记 PG_dirty,异步回写
脏页触发 周期、比例、显式同步、内存压力
回写 writeback 线程 → writepages → submit_bio → 清脏标记
关键参数 dirty_ratio、dirty_background_ratio、dirty_expire_centisecs、dirty_writeback_centisecs
调优 页缓存上限、swappiness、预读、IO 接口、drop_caches
应用 静态文件服务、数据库、日志写入
禁忌 禁止频繁 drop_caches;禁止 swappiness=0;核心数据要 fsync

7. 关键结论

  1. Page Cache 是 Linux I/O 性能的核心基石。

  2. 读操作先查页缓存,命中直接返回,未命中才读磁盘。

  3. 写操作先写页缓存,产生脏页,后续异步回写。

  4. 脏页回写保证内存与磁盘一致性,触发方式包括定时、比例、显式同步、内存压力。

  5. 预读、合并写、零拷贝、跨进程共享是页缓存高效的关键。

  6. 调优要结合业务:IO 密集型保留页缓存,计算密集型适当回收;大文件调大预读,小文件调小预读;核心数据用 fsync,大文件备份可用 O_DIRECT。

相关推荐
KING-WU5123 小时前
Linux GDB 调试实战指南:从编译到断点调试
linux·运维·服务器
Lucis__3 小时前
深入理解程序链接与加载—从动静态链接到库加载
linux·elf·链接·程序·动静态库
影视飓风TIM3 小时前
Linux 进程信号原理与剖析
linux·运维·服务器
缘友一世3 小时前
开源 Linux截图工具闪构(SnapForge)
linux·运维·ubuntu·开源·截图
朱包林4 小时前
iTerm2终端配置指南
linux·javascript·python·云原生·容器·kubernetes·云计算
祖力555 小时前
Linux系统移植、烧录与环境的搭建
linux·运维·服务器
张小姐的猫5 小时前
【Linux】网络编程 —— 多路转接:epoll
linux·运维·网络
宋明河6 小时前
五种 IO 模型与 select、poll、epoll 对比
linux
玄芯散人6 小时前
【金丹·81】性能分析:你的程序慢在哪
linux·性能优化·perf