Linux 内核源码分析-Page Cache 刷脏源码分析
准备
内核版本: 5.0
Page Cache是内核与存储介质的重要缓存结构,当我们使用write()或者read()读写文件时,假如不使用O_DIRECT标志位打开文件,我们均需要经过Page Cache来帮助我们提高文件读写速度。而在 MySQL 的设计实现中,读写数据文件使用了O_DIRECT标志,其目的是使用自身Buffer Pool的缓存算法。
根据之前总结的 Linux 内存管理文章,在 Linux 内核内存的基本单元是Page,而Page Cache也驻存于物理内存,所以Page Cache的缓存基本单位也是Page,而Page Cache缓存的内容属于文件系统,所以Page Cache属于文件系统与物理内存管理的枢纽。
介绍Page Cache必不可少的需要涉及VFS的内容,这里我们仅仅简单的介绍相关数据结构的具体含义,文件系统的实现细节暂且略过。Page Cache整个模块代码量巨大,我们侧重于Page Cache的刷脏策略分析。
Page Cache

Page Cache 相关数据结构
inode
include/linux/fs.h
inode在文件系统代表一个文件的元信息结构。
1 | struct inode { |
i_mapping代表inode所拥有的address_space
address_space
include/linux/fs.h
这里我们假定address_space缓存的Page来自于磁盘上的文件,而Page Cache并不是类似于 MySQL 中Buffer Pool一个缓存结构,它结合了于内核的内存管理和文件系统的address_space结构。address_space管理对应的文件映射在物理内存中缓存Page:
1 | struct address_space { |
host代表address_space所属的inode。i_pages代表该address_space缓存的Page。gfp_mask代表内存分配flags。i_mmap_writable代表共享内存映射的Page数量。i_mmap代表该address_space缓存的Page所存放的rb-tree。i_mmap_rwsem用来保护i_mmap和i_mmap_writable的自旋锁。nrpages代表该address_space缓存的Page数量。writeback_index代表回写时所使用的索引。a_ops代表address_space的操作方法函数。flags代表错误位。wb_err代表address_space最近操作方式的错误码。private_lock用来保护private_list的自旋锁。
address_space_operations
address_space_operations代表address_space支持的操作方法:
1 | struct address_space_operations { |
writepage:将Page写回磁盘。readpage: 从磁盘读取Page。writepages: 写多个Page至磁盘。set_page_dirty:设置某个Page为脏页。readpages: 读取多个Page, 一般用来预读。write_begin: 准备一个写操作。write_end: 完成一个写操作。invalidatepage:使该Page无效。releasepage:释放Page。direct_IO:对address_space中的所有Page进行DIO。
Page Cache 的插入
我们在Linux内核源码分析-内存请页机制中分析了缺页中断时,当访问的 Page Table 尚未分配,即vma对应磁盘上的某一个文件时,会调用vma->vm_ops->fault(vmf)对应的文件系统的缺页处理函数。
基本流程
1 | page = page_cache_alloc(); |
以ext4为例,ext4_filemap_fault()为缺页处理函数,具体调用了内存管理模块的filemap_fault()来完成:
1 | vm_fault_t filemap_fault(struct vm_fault *vmf) |
Page Cache 的插入主要流程如下:
- 判断查找的 Page 是否存在于 Page Cache,存在即直接返回
- 否则通过 Linux 内核物理内存分配介绍的伙伴系统分配一个空闲的 Page.
- 将 Page 插入 Page Cache,即插入
address_space的i_pages. - 调用
address_space的readpage()来读取指定 offset 的 Page.
Page Cache 的回写
假如 Page Cache 中的 Page 经过了修改,它的 flags 会被置为PG_dirty. 在 Linux 内核中,假如没有打开O_DIRECT标志,写操作实际上会被延迟刷盘,以下几种策略可以将脏页刷盘:
- 手动调用
fsync()或者sync强制落盘 - 脏页占用比率过高,超过了设定的阈值,导致内存空间不足,触发刷盘(强制回写).
- 脏页驻留时间过长,触发刷盘(周期回写).
在这里我们仅仅分析周期回写和强制回写
bdi
bdi是backing device info的缩写,它描述备用存储设备相关信息,就是我们通常所说的存储介质 SSD 硬盘等等。Linux 内核为每一个存储设备构造了一个backing_dev_info,假如磁盘有几个分区,每个分区对应一个backing_dev_info结构体.
backing_dev_info
1 | /* include/linux/backing-dev-defs.h */ |
bdi_list是全局维护的所有backing_dev_info链表.wb是脏页回写控制块.
bdi_writeback
1 | /* include/linux/backing-dev-defs.h */ |
bdi是该bdi_writeback所属的backing_dev_info.b_dirty代表文件系统中被修改的inode节点.b_io代表等待 I/O 的inode节点.dwork是一个封装的延迟工作任务,由它的主函数将脏页回写存储设备: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/* mm/backing-dev.c */
/* wb_init() 用来初始化 backing_dev_info */
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
int blkcg_id, gfp_t gfp)
{
...
INIT_LIST_HEAD(&wb->b_dirty);
INIT_LIST_HEAD(&wb->b_io);
INIT_LIST_HEAD(&wb->b_more_io);
INIT_LIST_HEAD(&wb->b_dirty_time);
spin_lock_init(&wb->list_lock);
wb->bw_time_stamp = jiffies;
wb->balanced_dirty_ratelimit = INIT_BW;
wb->dirty_ratelimit = INIT_BW;
wb->write_bandwidth = INIT_BW;
wb->avg_write_bandwidth = INIT_BW;
spin_lock_init(&wb->work_lock);
INIT_LIST_HEAD(&wb->work_list);
/* dwork的回调函数为wb_workfn() */
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
...
}
bdi_writeback对象封装了dwork以及需要处理的inode队列。当 Page Cache 调用__mark_inode_dirty()时,将需要刷脏的inode挂载到bdi_writeback对象的b_dirty队列上,然后唤醒对应的bdi刷脏线程。
wb_workfn()
wb_workfn是回写控制块的回调函数
1 | /* fs/fs-writeback.c */ |
wb_do_writeback分别实现了周期回写和后台回写两部分: wb_check_old_data_flush(),wb_check_background_flush(),具体实现我们分不同的场景分析,因为每一个存储设备都有一个backing_dev_info,所以每个存储设备之间的脏页回写互不影响.
周期回写
周期回写的时间单位是0.01s,默认为5s,可以通过/proc/sys/vm/dirty_writeback_centisecs调节:
1 | /* mm/page-writeback.c */ |
Page驻留为dirty状态的时间单位也为0.01s,默认为30s,可以通过/proc/sys/vm/dirty_expire_centisecs来调节:
1 | /* mm/page-writeback.c */ |
后台线程周期回写
1 | /* fs/fs-writeback.c */ |
强制回写
强制回写分为后台线程回写和用户进程主动回写。
当脏页数量超过了设定的阈值,后台回写线程会将脏页写回存储设备,后台回写阈值是脏页占可用内存大小的比例或者脏页的字节数,默认比例是10. 用户可以通过修改/proc/sys/vm/dirty_background_ratio修改脏页比或者修改/proc/sys/vm/dirty_background_bytes修改脏页的字节数。
而在用户调用write()接口写文件时,假如脏页占可用内存大小的比例或者脏页的字节数超过了设定的阈值,会进行主动回写,用户可以通过设置/proc/sys/vm/dirty_ratio或者/proc/sys/vm/dirty_bytes修改这两个阈值。
后台线程强制回写
1 | /* fs/fs-writeback.c */ |
用户进程触发回写
假如用户调用write()或者其他写文件接口时,在写文件的过程中,产生了脏页后会调用balance_dirty_pages调节平衡脏页的状态. 假如脏页的数量超过了**(后台回写设定的阈值+ 进程主动回写设定的阈值) / 2 **,即(background_thresh + dirty_thresh) / 2会强制进行脏页回写. 用户线程进行的强制回写仍然是触发后台线程进行回写
总结
触发 Page Cache 刷脏的几个条件如下:
- 周期回写,可以通过设置
/proc/sys/vm/dirty_writeback_centisecs调节周期. - 当后台回写阈值是脏页占可用内存大小的比例或者脏页的字节数超过了设定的阈值会触发后台线程回写.
- 当用户进程写文件时会进行脏页检查假如超过了阈值会触发回写,从而调用后台线程完成回写.
Page的写回操作是文件系统的封装,即address_space的writepage操作.
思考
因为Linux内核为每个存储设备都设置了刷脏进程,所以假如在日常开发过程遇到了刷脏压力过大的情况下,在条件允许的情况下,将写入文件分散在不同的存储设备,可以提高的写入速度,减小刷脏的压力.