当前位置: 首页 > news >正文

Linux内核学习轨迹第五部:页缓存Page Cache与回写机制(第九小节)

9. 页缓存Page Cache与回写机制

页缓存(Page Cache)是Linux内核用物理内存缓存磁盘文件数据的机制,是提升文件IO性能的核心手段。绝大多数的磁盘文件读写,都会经过页缓存,只有直接IO(O_DIRECT)会绕过页缓存。页缓存的本质是「用相对高速的内存,缓存相对低速的磁盘数据」,减少磁盘IO的次数,极大提升文件访问的性能。
很多工程师对页缓存的认知停留在「内存缓存文件」的表层,却不理解页缓存的架构、读写流程、脏页回写机制、预读策略,最终导致服务IO性能上不去、系统卡顿、内存占用过高等问题无法定位。本章节基于Linux 6.6 LTS内核,完整拆解页缓存的核心设计、数据结构、读写全流程、脏页回写机制、工程调优最佳实践。

9.1 页缓存的核心设计目标

  1. 减少磁盘IO,提升IO性能:用内存缓存磁盘文件的内容,重复访问文件时,直接从内存读取,不需要访问磁盘,IO延迟从毫秒级降到纳秒级;
  2. 统一缓存管理:为文件的读写、内存映射(mmap)、可执行文件加载提供统一的缓存,避免重复缓存,最大化内存利用率;
  3. 优化磁盘IO模式:把大量的小随机写,合并为大的顺序写,减少磁盘的寻道次数,提升磁盘的写入性能;
  4. 预读优化:基于文件的访问模式,提前读取后续的文件内容到页缓存,减少主缺页异常的次数,提升顺序读的性能;
  5. 内存复用:系统空闲内存越多,页缓存占用的内存越多,当应用程序需要内存时,页缓存可以被快速回收,分配给应用程序,实现内存的动态复用。

9.2 页缓存的核心数据结构

页缓存的核心锚点是struct address_space,每个打开的文件对应一个address_space实例,管理该文件的所有缓存页,定义在include/linux/fs.h中,我们在反向映射章节已经介绍过,这里重点拆解页缓存相关的核心字段。

9.2.1 核心锚点:struct address_space

struct address_space { // 宿主inode,对应磁盘上的文件 struct inode *host; // 页缓存的核心存储:XArray,存储该文件的所有缓存页 struct xarray i_pages; // 映射了该文件的所有VMA的区间树,用于反向映射 struct maple_tree i_mmap; // 保护i_mmap的读写信号量 rwlock_t i_mmap_rwsem; // 该文件的总缓存页数 unsigned long nrpages; // 脏页回写相关的操作函数集 const struct address_space_operations *a_ops; // 脏页、回写相关的标志位 gfp_t gfp_mask; unsigned int flags; // 错误记录 errseq_t wb_err; };
核心字段深度解析
  1. i_pages:XArray(扩展数组),Linux 4.20+替换了之前的radix树(基数树),是页缓存的核心存储结构。XArray是一个高效的、可扩展的稀疏数组,key是文件内的页偏移量(page->index),value是对应的struct page结构体指针。通过文件偏移量,可以在O(logn)时间复杂度内快速找到对应的缓存页;
  2. a_ops:地址空间操作函数集,定义了该文件系统的页缓存操作函数,包括读页、写页、脏页回写、释放页等,不同的文件系统(ext4、xfs、btrfs)有不同的实现;
  3. host:指向文件的inode结构体,inode是磁盘文件在内存中的表示,存储了文件的元数据(权限、大小、创建时间、块指针等),address_space和inode是一一对应的关系。

9.2.2 缓存页的状态标志

页缓存中的每个物理页,通过struct page的标志位,标记页的状态,是页缓存处理逻辑的核心依据,高频标志位:

标志位

核心含义

页缓存中的作用

PG_locked

页被锁定

正在进行IO操作(读/写磁盘)时设置,防止并发访问导致的数据竞争,IO完成后清除

PG_uptodate

页的数据是最新的

从磁盘成功读取数据到页后设置,标记页的数据和磁盘一致,可以直接访问

PG_dirty

页是脏页

页的内容被修改,和磁盘不一致,需要回写到磁盘

PG_writeback

页正在回写

页正在被回写到磁盘,回写完成后清除该标志,同时清除PG_dirty

PG_referenced

页最近被访问过

用于LRU链表的活跃/不活跃判断,标记页的冷热程度,决定回收优先级

PG_active

页在活跃LRU链表中

标记页是活跃的,最近被频繁访问,回收优先级低

PG_private

页有私有数据

用于文件系统的缓冲区,比如ext4的buffer_head

9.3 页缓存的读流程与预读机制

Linux的文件读操作,分为两种:缓冲IO(Buffered IO)和直接IO(Direct IO)。缓冲IO会经过页缓存,是默认的IO模式;直接IO会绕过页缓存,直接和磁盘交互,适用于数据库等自己实现缓存的场景。我们重点拆解缓冲IO的读全流程。

9.3.1 缓冲IO的读全流程

用户态调用read()系统调用读取文件时,缓冲IO的完整内核执行流程:
sys_read() → ksys_read() → vfs_read() → __vfs_read() → file->f_op->read_iter()
1. 通用文件读接口:generic_file_read_iter()
2. 计算文件内的读取偏移量和长度,按页大小拆分,遍历每个需要读取的页
3. 查找页缓存:filemap_get_pages()
├→ 以页偏移为key,在address_space->i_pages XArray中查找对应的缓存页
├→ 如果页已经在缓存中,且PG_uptodate=1,直接跳到步骤6,从缓存页复制数据到用户态缓冲区
└→ 如果页不在缓存中,或者PG_uptodate=0,进入步骤4,从磁盘读取
4. 分配缓存页,加入页缓存
├→ 调用alloc_page()分配一个空白物理页
├→ 把页加入到address_space->i_pages XArray中,设置PG_locked标志,锁定页
├→ 把页加入到LRU链表的不活跃链表中,等待内存回收
5. 从磁盘读取数据到缓存页
├→ 调用address_space->a_ops->readpage(),提交IO请求到块层
├→ 等待磁盘IO完成,中断处理函数唤醒等待的进程
├→ IO完成后,设置页的PG_uptodate标志,清除PG_locked标志
6. 数据复制:copy_page_to_iter()
├→ 把缓存页中的数据,复制到用户态的缓冲区
├→ 更新文件的访问时间atime
7. 触发文件预读:page_cache_sync_readahead()/page_cache_async_readahead()
├→ 内核根据文件的访问模式,判断是顺序读还是随机读
├→ 如果是顺序读,提前读取后续的若干页到页缓存中
8. read()系统调用返回,读取完成
核心工程细节
  1. 读操作的核心优化:重复读取同一个文件时,数据已经在页缓存中,不需要访问磁盘,直接从内存复制,性能提升上千倍;
  2. 页缓存的生命周期:只要系统有空闲内存,页缓存就会一直保留,直到内存不足时,内核才会回收不活跃的页缓存;
  3. 预读机制是顺序读性能的核心,提前把后续的页加载到缓存中,避免每次读取都触发磁盘IO,减少主缺页异常的次数。

9.3.2 文件预读机制

预读(Readahead)是Linux优化文件顺序读性能的核心机制,内核会根据进程的文件访问模式,预测进程后续会访问的内容,提前读取到页缓存中,减少磁盘IO的次数。
预读的核心规则

1.顺序访问检测:内核会跟踪进程的文件访问记录,如果发现进程连续访问文件的后续页,判断为顺序读,触发预读;

2.预读窗口动态调整:预读窗口的大小会根据访问模式动态调整,初始预读窗口是4页,顺序访问持续的话,预读窗口会逐步增大,最大到128页(512KB);

3.同步预读与异步预读:

  1. 同步预读:当进程访问的页不在缓存中时,同步读取当前页,同时预读后续的页,阻塞等待IO完成;
  2. 异步预读:当进程访问的页已经在缓存中,但是已经到达预读窗口的边缘时,后台异步触发预读,读取后续的页,不会阻塞进程的读操作;

4.随机访问处理:如果内核检测到进程是随机访问文件,会关闭预读,避免不必要的磁盘IO,浪费内存和带宽。

预读的用户态控制
用户态可以通过posix_fadvise()系统调用,告诉内核文件的访问模式,调整预读行为:
int posix_fadvise(int fd, off_t offset, off_t len, int advice);

常用advice参数:

  1. POSIX_FADV_NORMAL:默认预读行为,适合普通的顺序访问;
  2. POSIX_FADV_SEQUENTIAL:告诉内核进程会顺序访问文件,开启更大的预读窗口,预读大小翻倍;
  3. POSIX_FADV_RANDOM:告诉内核进程会随机访问文件,关闭预读;
  4. POSIX_FADV_WILLNEED:告诉内核进程会很快访问这段文件内容,提前把内容加载到页缓存中,异步预读;
  5. POSIX_FADV_DONTNEED:告诉内核进程不会再访问这段内容,释放对应的页缓存,回收内存。

9.4 页缓存的写流程与脏页回写机制

文件的写操作,默认也是缓冲IO,不会直接写入磁盘,而是先写入页缓存,把页标记为脏页(PG_dirty),然后由内核的回写线程,异步把脏页回写到磁盘中。这种「延迟写」机制,极大提升了写操作的性能,同时可以合并小的随机写为大的顺序写,优化磁盘IO模式。

9.4.1 缓冲IO的写全流程

用户态调用write()系统调用写入文件时,缓冲IO的完整内核执行流程:
sys_write() → ksys_write() → vfs_write() → __vfs_write() → file->f_op->write_iter()
1. 通用文件写接口:generic_file_write_iter()
2. 检查文件的写入权限、是否追加模式、是否有文件锁,更新文件的修改时间mtime
3. 计算文件内的写入偏移量和长度,按页大小拆分,遍历每个需要写入的页
4. 查找/分配缓存页:filemap_get_pages()
├→ 以页偏移为key,在address_space->i_pages XArray中查找对应的缓存页
├→ 如果页已经在缓存中,锁定页,跳到步骤6
└→ 如果页不在缓存中,分配新的物理页,加入页缓存,锁定页
5. 对于文件的空洞写入(超出文件大小的写入),预分配磁盘块,更新inode的大小
6. 数据复制:copy_page_from_iter()
├→ 把用户态缓冲区的数据,复制到缓存页中
├→ 如果是整页写入,直接设置PG_uptodate标志;如果是部分写入,需要先读取页的原有内容,再合并写入
7. 标记脏页:__set_page_dirty()
├→ 设置页的PG_dirty标志,标记为脏页,需要回写到磁盘
├→ 把页加入到对应BDI的脏页链表中,等待回写线程处理
├→ 清除页的PG_locked标志,解锁页
8. write()系统调用返回,写入完成(数据仅在页缓存中,还未写入磁盘)
9. 内核回写线程异步把脏页回写到磁盘
核心工程细节
  1. 缓冲IO的write()调用,只是把数据复制到页缓存就返回,不会等待磁盘IO完成,延迟极低,性能极高;
  2. 数据写入页缓存后,在回写到磁盘之前,如果系统崩溃、断电,数据会丢失,这是缓冲IO的核心风险;
  3. 脏页回写是异步的,由内核的回写线程统一管理,合并多个小的写操作,批量写入磁盘,减少磁盘寻道次数,提升写入性能。

9.4.2 脏页回写机制

脏页回写由内核的bdi_writeback线程(之前的pdflush/flush线程)负责,每个块设备(BDI,Block Device Interface)对应一个回写线程,避免单个线程成为性能瓶颈。
脏页回写的三大触发条件
  1. 定时回写:回写线程会周期性唤醒,检查系统中的脏页,把超过过期时间的脏页回写到磁盘,默认过期时间是30秒;
  2. 内存阈值触发回写:当系统中的脏页数量超过了设定的阈值,内核会唤醒回写线程,开始回写脏页,避免脏页占用过多的内存;
  3. 主动同步触发回写:用户态调用fsync()/fdatasync()/sync()系统调用,主动把脏页回写到磁盘,等待IO完成后返回,保证数据持久化。

9.4.3 脏页回写的核心调优参数

脏页回写的行为由/proc/sys/vm/下的参数控制,是IO性能调优的核心,核心参数:

参数名

默认值

核心含义

调优场景

dirty_background_ratio

10

脏页占总内存的百分比达到该值时,唤醒回写线程异步回写脏页

写入量大的场景,调大该值,减少回写次数,提升写入性能;稳定性要求高的场景,调小该值,减少数据丢失风险

dirty_ratio

20

脏页占总内存的百分比达到该值时,所有的写操作会被阻塞,同步等待脏页回写

写入量大的场景,调大该值,避免写操作被阻塞;稳定性要求高的场景,调小该值,限制脏页的最大数量

dirty_expire_centisecs

3000

脏页的过期时间,单位百分之一秒,默认30秒,脏页超过这个时间会被回写

数据安全性要求高的场景,调小该值,比如300(3秒),减少断电时的数据丢失;写入性能要求高的场景,调大该值,合并更多的写操作

dirty_writeback_centisecs

500

回写线程的唤醒周期,单位百分之一秒,默认500ms

写入频繁的场景,调小该值,让回写线程更频繁的唤醒,避免脏页堆积;写入少的场景,调大该值,减少CPU开销

dirtytime_expire_seconds

43200

脏inode的过期时间,默认12小时,超过这个时间,inode的元数据会被回写

元数据修改频繁的场景,调小该值,保证元数据的持久化

9.5 工程实践与避坑指南

1.free命令中内存字段的认知纠正

free命令的输出解析:

total used free shared buff/cache available Mem: 32742344 8123456 2098765 123456 22520123 23876543 Swap: 16777212 0 16777212

total:总物理内存;

used:已经被进程、内核占用的内存,不包括buff/cache;

free:完全空闲的物理内存;

buff/cache:页缓存、缓冲区、slab占用的内存,这部分内存可以被快速回收,当应用程序需要内存时,内核会立即回收这部分内存,分配给应用程序;

available:系统真正可用的内存,等于free + 可回收的buff/cache,是判断系统内存是否充足的核心指标。

认知纠正:Linux会尽可能利用空闲内存做页缓存,提升IO性能,所以buff/cache占用高是正常的,不是内存泄漏,只有当available列很低时,才说明系统内存不足。

2.页缓存的性能优化最佳实践

  1. 顺序读优化:大文件顺序读取时,用posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL)告诉内核顺序访问,开启更大的预读窗口,提升读取性能;
  2. 随机读优化:小文件随机读取时,用posix_fadvise(fd, offset, len, POSIX_FADV_WILLNEED)提前把需要的内容加载到页缓存中,避免读取时阻塞;对于完全随机的访问,用POSIX_FADV_RANDOM关闭预读,避免不必要的磁盘IO;
  3. 写入性能优化:写入量大的场景,适当调大dirty_background_ratio和dirty_expire_centisecs,让内核合并更多的写操作,减少磁盘IO次数,提升写入性能;但要注意,调大这些值会增加断电时的数据丢失风险;
  4. 数据安全优化:数据库等对数据持久化要求高的场景,每次事务提交必须调用fsync()/fdatasync(),把脏页和元数据强制回写到磁盘,保证数据不丢失;同时调小dirty_expire_centisecs,减少脏页在内存中的停留时间;
  5. 缓存释放:需要释放页缓存,回收内存时,可以用以下命令:
# 释放页缓存 echo 1 > /proc/sys/vm/drop_caches # 释放目录项和inode缓存 echo 2 > /proc/sys/vm/drop_caches # 释放页缓存、目录项、inode缓存 echo 3 > /proc/sys/vm/drop_caches

注意:drop_caches只能释放干净的、没有被锁定的页缓存,脏页需要先回写到磁盘才能被释放,所以执行drop_caches之前,先执行sync命令,把脏页回写到磁盘。

3.直接IO的适用场景与避坑

适用场景:

  1. 数据库等自己实现了缓存机制的应用,不需要内核的页缓存,避免双重缓存,浪费内存;
  2. 大文件的顺序读写,不需要预读和缓存;
  3. 对数据持久化要求极高的场景,直接写入磁盘,避免页缓存中的数据丢失。

避坑指南:

  1. 直接IO的缓冲区必须按磁盘扇区大小(通常512字节/4KB)对齐,否则会调用失败;
  2. 直接IO的读写会阻塞等待磁盘IO完成,延迟比缓冲IO高很多,不适合小的随机读写;
  3. 直接IO会绕过页缓存,无法利用内核的预读、合并写优化,随机读写性能极差;
  4. 直接IO和缓冲IO混合使用同一个文件,会导致数据不一致,必须避免。

4.脏页回写导致的系统卡顿问题排查与解决

很多场景下,系统会出现周期性的卡顿,IO等待高,CPU的iowait很高,根源是脏页回写的配置不合理,大量脏页集中回写,阻塞了正常的IO操作。

排查流程:

  1. 用vmstat 1查看bi/bo(磁盘IO)、wa(iowait),确认卡顿的时候是否有大量的磁盘写入;
  2. 用cat /proc/meminfo | grep Dirty查看脏页的数量,确认卡顿的时候脏页数量是否突然下降,说明正在集中回写脏页;
  3. 检查dirty_ratio和dirty_background_ratio的配置,如果两个值太接近,会导致异步回写还没完成,就达到了dirty_ratio,阻塞所有写操作,导致系统卡顿。

解决方案:

  1. 调大dirty_background_ratio和dirty_ratio的差值,比如dirty_background_ratio=10,dirty_ratio=30,保证异步回写有足够的时间,不会触发同步阻塞;
  2. 调小dirty_writeback_centisecs,让回写线程更频繁的唤醒,每次回写少量的脏页,避免集中回写;
  3. 调小dirty_expire_centisecs,让脏页更快的被回写,避免脏页堆积;
  4. 对于机械硬盘,开启IO调度器的合并优化,用mq-deadline调度器;对于SSD,用none/kyber调度器,提升IO性能。

5.页缓存的监控工具

  1. 全局监控:vmstat 1、sar -r 1、free -h,查看系统的页缓存、脏页数量、内存使用情况;
  2. 进程级监控:pcstat工具,查看某个文件在页缓存中的占用比例;mincore系统调用,查看进程的虚拟地址对应的页是否在缓存中;
  3. 详细监控:cat /proc/meminfo,查看Cached、Dirty、Writeback、PageTables等详细指标;
  4. 回写监控:cat /proc/vmstat | grep pgpgin | pgpgout | pswpin | pswpout,查看磁盘IO的读写次数;iostat -x 1,查看磁盘的IOPS、吞吐量、iowait、队列长度。
http://www.jsqmd.com/news/970425/

相关文章:

  • 前端初学者如何深度理解 如何创建一个路由页面
  • 【Android】 VidFetch一键下载各大平台视-内置播放器
  • PI XLs Designer v8.0:电源变压器设计的精密计算与深度优化指南
  • 2026荔湾区搬家公司终极评测排行|全域覆盖、价格透明、安全保障深度实测避坑指南 - gzdjxd
  • MonkeyCode从入门到精通:完整使用指南
  • Windows下开箱即用的音视频转码工具包,含全格式编解码支持
  • Linux 下删库跑路的正确姿势?别怕,教你数据恢复全流程
  • 2026国内最有名起名老师推荐.起名大师推荐. - 资讯纵览
  • FitGirl游戏启动器完整指南:一站式管理压缩游戏的终极解决方案
  • SpringBoot+Vue 农商对接系统管理平台源码【适合毕设/课设/学习】Java+MySQL
  • 蚂蚁搬家难易程度划分
  • 告别臃肿安装!手把手教你为Zynq-7000定制最小化的Vivado 18.3开发环境
  • 3分钟免费激活Windows和Office:KMS_VL_ALL_AIO一键智能激活方案
  • GraphRAG 生产配置:多模型策略怎么选,成本怎么控
  • 2026白云区搬家公司终极评测排行|全域覆盖+价格透明+安全保障优质服务商全解析 - gzdjxd
  • 晶振采购实战指南:从参数到供应链,保障电子项目稳定心跳
  • 抖音视频无水印解析工具:3步获取纯净版短视频的终极方案
  • 石家庄起名馆排名.石家庄起名老师推荐.石家庄起名大师推荐 - 资讯纵览
  • 在Ubuntu 22.04上,5分钟搞定CloudCompare的Snap安装与基础点云查看
  • WzComparerR2技术解析:冒险岛WZ文件逆向工程的完整实现方案
  • 基于PID的直流电机伺服控制系统 + AI
  • React Native 应用适配鸿蒙PC 实战:从白屏到成功运行
  • 从零构建3D打印切片软件:BambuStudio开源贡献实战指南
  • 高光谱图像ROI区域Gabor纹理特征自动优选MATLAB工具包(含GA参数优化与PLS建模)
  • 终极指南:用EPubBuilder实现浏览器端EPUB编辑的完整方案
  • 第29届国际C语言混乱代码大赛:参赛作品数量质量双高,亮点多多!
  • 嵌入式ADC滤波:跳水算法原理、实现与优化
  • 深度解析Realtek RTW89无线网卡驱动:Linux系统下WiFi 6/7设备完整技术指南
  • 发物流怎么收费?2026最新计费标准全解析 - 快递物流资讯
  • 【毕业设计】SpringBoot+Vue+MySQL 实习管理系统平台源码+数据库+论文+部署文档