从Page Cache的产生机理到标准I/O与存储映射I/O的对比

前言

Page Cache 即页高速缓冲存储器,由多个物理页(page)构成;一个page为4KB,所以PageCache大小通常为4KB的整数倍。

我们知道文件一般存放在硬盘(机械硬盘或固态硬盘)中,CPU 并不能直接访问硬盘中的数据,而是需要先将硬盘中的数据读入到内存中,然后才能被 CPU 访问。(硬盘属于磁盘,是硬磁盘,计组里经常说磁盘)
由于读写硬盘的速度比读写内存要慢很多(DDR4 内存读写速度是机械硬盘500倍,是固态硬盘的200倍),所以为了避免每次读写文件时,都需要对硬盘进行读写操作,Linux 内核使用 页缓存(Page Cache) 机制来对文件中的数据进行缓存。

以下主要来自【极客时间—linux内核技术实战课程】的笔记,另外参考资料链接:
https://cloud.tencent.com/developer/article/1848933
https://www.sohu.com/a/290524170_467784

一、Page Cache初印象

为了提升对文件的读写效率,Linux 内核会以页大小(4KB)为单位,将磁盘中的文件划分为多数据块。当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为 页缓存即page cache)与文件中的数据块进行绑定,下面是一个page cache,内核中会有很多个。
在这里插入图片描述
图源见水印。

也就说是,当用户对文件进行读写时,实际上是对文件的 页缓存Page Cache 进行读写。所以对文件进行读写操作时,会分以下两种情况进行处理:

(1)当从(磁盘)文件中读取数据时,内核会先看page cache中有没有要读取的数据,如果有,那么直接从内存中读取,不需要访问磁盘, 此即 cache hit(缓存命中);如果没有,那就将该数据从磁盘中拷贝到page cache中,这样下次读这个数据的时候就不需要再从磁盘里读了。这里面应该有一个LRU策略。— —page cache可以只保存一个文件中的一部分内容,而不需要把整个文件保存进来,保存的最小单位是磁盘的block,通常也是4KB大小。

所以平时我们读某个文件,第一次读的时候可能很耗时,但是第二次读的时候会快很多,因此该文件已经在内存的page cache中了

(2)当向文件中写入数据时,同理。如果要修改的数据已经存在于在page cache中,那内核直接往page cache中写入,如果没有,那就新申请一个page cache,将要修改的数据从磁盘读入page cache,再修改。内核会将被写入的page标记为dirty, 并将其加入到dirty list中,并且会周期性地将dirty list中的page写回到磁盘上, 从而使磁盘上的数据和内存中缓存的数据一致

二、内核中对Page Cache的实现

在 Linux 内核中,文件的每个数据块最多只能对应一个 Page Cache 项,它通过两个数据结构来管理这些 Cache 项,一个是 radix tree,另一个是 双向链表。其中,文件的cache项就是由radix来管理的,下面会详细介绍radix管理文件的page cache。

双向链表:Linux内核为每一片物理内存区域(zone)维护active_list和inactive_list两个双向链表,这两个list主要用来实现物理内存的回收

radix tree 是一种多叉搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 page cache 项,radix tree的结构示意图如下:
在这里插入图片描述

每个文件有多个page cache,所以radix的每个节点就是一个page cache。

在 Linux 内核中,使用 file 对象来描述一个被打开的文件,其中有个名为 f_mapping 的字段,定义如下:
struct file { ... struct address_space *f_mapping; };
f_mapping 字段的类型为 address_space 结构,其定义如下:

struct address_space {
    struct inode           *host;      /* owner: inode, block_device */
    struct radix_tree_root page_tree;  /* radix tree of all pages */
    rwlock_t               tree_lock;  /* and rwlock protecting it */
    ...
};

address_space 结构其中的一个作用就是用于存储文件的 页缓存,下面介绍一下各个字段的作用:

host:指向当前 address_space 对象所属的文件 inode 对象(每个文件都使用一个 inode 对象表示)。
page_tree用于存储当前文件的 页缓存
tree_lock:用于防止并发访问 page_tree 导致的资源竞争问题。
从 address_space 对象的定义可以看出,文件的 页缓存 使用了 radix树 来存储。

radix树:使用键值(key-value)对的形式来保存数据,并且可以通过键快速查找到其对应的值。内核以文件读写操作中的数据 偏移量
作为键,以数据偏移量所在的 页缓存 作为值,存储在 address_space 结构的 page_tree 字段中

三、page cache在内核中的图像

在这里插入图片描述
由上图可以看到标准 I/O内存映射mmap会先把数据写入到 Page Cache,这样做会通过减少 I/O 次数来提升读写效率

四、Page Cache 的构成

下面等式两边的内容就是我们平时说的 Page Cache(用$ cat /proc/meminfo 查看)。两边都有SwapCached,之所以要把它放在等式里,就是说它也是 Page Cache 的一部分:

Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem +SwapCached

标红的两个是最需要注意的。因为平时用的 mmap() 内存映射方式和 buffered I/O(标准I/O)来消耗的内存就属于这部分,最重要的是,这部分在真实的生产环境上也最容易产生问题。

五、Page Cache 的产生

由第三部分的图像就知道有两种产生方式:
Buffered I/O(标准 I/O)
Memory-Mapped I/O(存储映射 I/O)— —效率高

标准IO就是缓存IO,涉及内核和用户的缓存区间的拷贝,mmap说是存储映射,内存映射都行,虽然也涉及到磁盘,但是主要是针对内存。
下图是两种产生方式的示意图,其中的Page就是物理页,但我们这是专门将page cache,所以这些页也叫PageCache page,即属于page cache 的page。
在这里插入图片描述

标准 I/O和存储映射 I/O的差异:

(1)标准 I/O 是写的 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲
区里的数据拷贝到内核缓冲区
(Pagecache Page 对应的内存);如果是读的 (read(2)) 话则
是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容
不存在任何映射关系。(不是映射,而是写后拷贝。)

(2)对于存储映射 I/O 而言,则是直接将 Pagecache Page 给映射到用户地址空间,用户直接
读写 Pagecache Page 中内容。

显然,存储映射 I/O 要比标准 I/O 效率高一些,毕竟少了“用户空间到内核空间互相拷贝”的过程。这也是很多应用开发者发现,为什么使用内存映射 I/O 比标准 I/O 方式性能要好一些的主要原因。 但标准IO更常用。

比如我们写一个新文件:

#写一个新文件,该文件的大小为1048576 KB
dd if=/dev/zero of=$NEW_FILE bs=1024 count=1048576 &> /dev/null

那么Page Cache 就会增加这么多,其中增加的就是Inactive(File) 这一项。这一过程就是标准 I/O ,其中写文件涉及的内核机制如下图:
在这里插入图片描述
上述过程简述:
首先往用户缓冲区 buffer(这是 Userspace Page) 写入数据,然后 buffer 中的数据拷贝到内核缓冲区(这是 Pagecache Page),如果内核缓冲区中还没有这个 Page,就会发生 Page Fault 会去分配一个 Page,拷贝结束后该 Pagecache Page 是一个 Dirty Page(脏页),然后该 Dirty Page 中的内容会同步到磁盘,同步到磁盘后,该 Pagecache Page 变为 Clean Page 并且继续存在系统中。
【可以将 Alloc Page 理解为 Page Cache 的“诞生”,将 Dirty Page 理解为Page Cache 的婴幼儿时期(最容易生病的时期),将 Clean Page 理解为 Page Cache的成年时期(在这个时期就很少会生病了)】

但如果是读文件,那就跟一出生就是成人一样,它产生的 Page Cache的内容跟磁盘内容是一致的,所以它一开始是 Clean Page,除非改写了里面的内容才会变成 Dirty Page。

六、Page Cache 的回收

free命令可以查看系统内存使用情况
在这里插入图片描述
申请和回收Page Cache的过程如下图,可以看到Page Cache的回收有两种形式,后台回收直接回收
在这里插入图片描述
可以看到,应用在申请内存的时候,即使没有空闲内存(free page),只要还有足够可回收的 Page Cache,就可以通过回收 Page Cache 的方式来申请到内存。
而回收的方式主要是两种:直接回收和后台回收。

后台回收:后台回收是有一个内核线程kswapd来做的,当内存里free的pages低于一个水位(page_low)时,就会唤醒该内核线程,然后它从LRU链表里回收page cache到内存的free_list里头,它会一直回收直至free的pages达到另外一个水位page_high。

直接回收:在进程申请内存发生page fault时,发现没有足够可用的内存,于是进程(线程)就自己直接去回收内存,它一次性的会回收32个pages。进程会一直阻塞等待,直到有可用的page cache。— —其实就是进程内多个线程,一些是工作线程,一个是被启动专门用于回收page cache,这个线程不执行完,程序就一直阻塞在这。
所以要避免直接回收,看上图就知道是优先后台回收。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值