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内核为每个存储设备都设置了刷脏进程,所以假如在日常开发过程遇到了刷脏压力过大的情况下,在条件允许的情况下,将写入文件分散在不同的存储设备,可以提高的写入速度,减小刷脏的压力.