操作系统之Page Cache
Page Cache是通过将磁盘中的数据缓存到内存中,减少磁盘I/O操作,从而提高性能,提高性能主要基于两个因素:一:磁盘访问速度比内存慢几个数量级。第二是访问过的数据,很大概率再次访问(局部性原理)。此外,还要确保Page Cache中的数据更改能够同步到磁盘上,这称之为page回写(page writeback)。一个inode对应一个page cache对象,一个page cache对象包含多个物理page。
1. 什么是Page Cache
Page Cache是内核管理的内存,也就是说,它属于内核,而不属于用户。
在Linux上,可以直接查看Page Cache的方式有很多, 包括/proc/meminfo、free 、/proc/vmstat 命令,它们的内容实际上是一致的。
以/proc/meminfo命令来说:
makefile
# cat /proc/meminfo
...
Buffers: 0 kB
Cached: 1286056 kB
SwapCached: 0 kB
Active: 14567748 kB
Inactive: 590956 kB
Active(anon): 14328396 kB
Inactive(anon): 278896 kB
Active(file): 239352 kB
Inactive(file): 312060 kB
...
Shmem: 734644 kB
Slab: 88756 kB
SReclaimable: 38556 kB
...
其中:Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached
等式两边的内容就是平时说的Page Cache,两边都有SwapCached,只是为了说明它也是Page Cache的一部分。
在Page Cache中,Active(file) + Inactive(file) 是File-backed page(与文件对应的内存页),是最需要关注的部分,因为平时用的mmap()内存映射方式和buffered I/O来消耗的内存就是这部分。
SwapCached是打开了Swap分区后,把Inactive(anno) + Active(anno)两项匿名页交换到磁盘(swap out)之后,然后在读入到内存(swap in)之后分配的内存。由于读入到内存后,原来的Swap File还存在,所以SwapCached也可以认为是File-backed page,属于Page Cache。 注意:SwapCached只有在Swap分区打开的请求下才会有,但是Swap过程产生的I/O很容易引起性能抖动,因此,线上环境Swap分区一般是关闭的。
Shmen是指匿名共享映射这种方式分配的内存(free 命令中的shared一项)。
1.1 读Cache
当内核发起一个读请求时,会先检查请求的数据是否缓存到了page cache中,如果有则直接从内存中读取,不需要访问磁盘。如果cache没有请求的数据,就必须从磁盘中读取数据,然后内核将数据缓存到cache中。这样后续读请求就可以命中cache了。page可以只缓存一个文件部分的内容,不需要把整个文件都缓存进来。
1.2 写Cache
当内核发起一个写请求时,同样直接写入到cache中。内核会将被写入的page标记为dirty,并将其加入到dirty list中。内核会周期性的将dirty list中的page回写到磁盘上。从而使磁盘上的数据和内存中缓存的数据一致。
2. Page Cache 产生
Page Cache的产生有两种不同的方式:
- Buffered I/O(标准I/O)
- Memory-Mapped I/O(存储映射I/O)
两种方式产生如下图:
对于标准I/O的写,是写用户缓冲区,然后再讲用户缓冲区的数据拷贝到内核缓冲区。如果是读的话,则先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区中读数据。
对于存储映射I/O,是直接将Pagecache 的Page 给映射到用户地址空间,用户直接读写Pagecache Page中的内容。
以标准I/O为例,解释一下,Page Cache如何产生。具体如下图:
1)往用户缓冲区buffer写入数据。然后buffer中的数据拷贝到内核缓冲区(Pagecache page)。
2)如果内核缓冲区中没有这个Page,就会发生Page Fault,会去分配一个Page。
3)拷贝数据,该Pagecache Page就是一个Dirty Page(脏页)。
4)然后Dirty Page的内容会同步到磁盘,同步到磁盘后,该Pagecache Page就会变成Clean Page并且继续存在系统中。
如果是读文件产生的PageCache ,它的内容和磁盘内容一样,所以它一开始就是Clean Page,除非改写了里面的内容才会变成Dirty Page。
2.1 查看脏页
cat /proc/vmstat | egrep "dirty|writeback"
nr_dirty 表示当前系统中积压了多少脏页,nr_writeback 则表示有多少脏页正在回写到磁盘中,他们两个的单位都是 Page(4KB)。
3. Page Cache回收
先看写内存分配的图:
可以得出,应用在申请内存的时候,即使没有free内存了,只要还有足够的可回收逇Page Cache,也可以通过回收Page Cache的方式来申请到内存,所以,回收的方式主要有两种:直接回收和后台回收。
也就是对应的两种页面回收机制:
1)周期性的检查:后台运行的守护进程kswapd完成。该进程定期检查当前系统的内存使用情况,发现系统内空闲的物理内存数目少于特定的阈值(参数是什么?),该进程就会发起页面回收的操作。
2)"内存严重不足"事件触发:如果需要很大内存,而当时系统的内存没有办法提供足够多的物理内存以满足内存请求。这时,操作系统就必须尽快进行页面回收,以便释放一些内存空间从而满足内存请求。
可以通过命令sar来观察内存回收行为,也可以通过查看/proc/vmstat里面的指标进行查看。
3.1 回收算法
Linux中的页面回收是基于LRU(Lease recently used ,最近最少使用)算法。Linux操作系统对LRU的实现是基于一对双向链表,active链表和inactive链表。经常被访问的处于活跃状态的页面会被放到activre链表上,并不经常使用的页面则会放到inactive聊表上。页面会在两个双向链表中移动。页面可能从active链表移动到inactive链表,反之也有可能。但是移动并不是每次页面访问都会发生(要通过自旋锁来保证对链表并发访问操作不会出错,为了降低锁竞争,LInux提供了一种特殊的缓存,LRU缓存,用于批量的向LRU链表中快速的添加页面。有了 LRU 缓存之后,新页不会被马上添加到相应的链表上去,而是先被放到一个缓冲区中去,当该缓冲区缓存了足够多的页面之后,缓冲区中的页面才会被一次性地全部添加到相应的 LRU 链表中去),页面的移动发生的间隔有可能比较长。对于最近最少使用的页面会被逐个放到inactive链表的尾部。进行页面回收时,Linux操作系统会从inactive链表的尾部开始回收。
3.2 Active和Inactive
而第一次读取文件后,文件内容都是Inactive的,只有再次读取这些内容后,才会把它放到active链表上。处于Inactive链表上的pagecache在内存紧张的时候,是会首先被回收掉。有很多情况下,文件内容往往只被读取一次,它们占用的pagecache需要首先被回收掉;对于业务数据,往往都会读取几次,那么他们就会被放到active链表上,以此来达到保护的目的。
在内存紧张的情况下,会进行内存回收,回收会把Inactive list的部分page给回收掉。为了维护inactive和active的平衡,就需要把active list的部分page给demote到inactive list,demote的原则也是LRU。
3.3 匿名页
不是file-backed pages,即为匿名页(anonymous page),如堆、栈和数据段等,不是以文件形式存在,因此无法和磁盘文件交换,但可以通过磁盘上划分额外的swap分区或使用swap文件进行交换。
3.4 回收机制
水位(watermark)控制
min:如果剩余内存减少到触及这个水位,可认为内存严重不足,当前进程就会被阻塞,kernel会直接在这个进程的进程上下文做内存回收(direct reclaim)。
low:当剩余内存慢慢减少,触及到这个水位,就会触发kswapd线程进行内存回收。(后台回收)
high:进行内存回收时,内存慢慢增加,触及到这个水位时,就停止回收。
由于每个ZONE是分别管理各自的内存的,因此每个ZONE都有这个三个水位。
水位计算: /proc/sys/vm/min_free_kbytes 是一个用户可配置的值,默认值是min_free_kbytes = 4 * sqrt(lowmem_kbytes)。然后根据min算出来low和high水位的值:low=5/4min,high=6/4min。(计算不是这里的重点,如果有需要见参考资料7)
swapness
回收的时候,会回收file-backed page和 anonymous page ,但是谁回收的多一些,可以通过/proc/sys/vm/swapness来控制谁回收多一些。swapness的值越大,越倾向于回收匿名页。值越小,越倾向于回收file-backed的页面。回收方法都是LRU算法。
4. Page Cache 回写
Page cache毕竟是为了提高性能占用的物理内存,随着越来越多的磁盘数据被缓存到内存中,Page Cache也变得越来越大,如果一些重要的任务需要被Page cache占用的内存,内核将回收page cache以此来支持。
4.1 page write back的触发条件
1.空间层面
当系统的"dirty"的内存大于某个阈值,该阈值是在总共的"可用内存"(包括free pages 和reclaimable pages)中的占比。
参数"dirty_background_ratio"(默认值10%),或者是绝对字节数"dirty_background_bytes"(默认值为0,表示生效)。两个参数只要谁先达到即可执行,此时就会交给专门负责writeback的background线程去处理。
参数"dirty_ratio"(默认值30%)和"dirty_bates"(默认值为0,表示生效),当"dirty"的内存达到这个比例或数量,进程则会停下write操作(被阻塞),先把"dirty"进行writeback。
2.时间层面
周期性的扫描,扫描间隔用参数:dirty_writeback_interval表示,以毫秒为单位。发现存在最近一次更新时间超过某个阈值(参数:dirty_expire_interval,单位毫秒)的pages。如果每个page都维护最近更新时间,开销会很大且扫描会很耗时,因此具体实现不会以page为粒度,而是按inode中记录的dirtying-time来计算。
3.用户主动发起。
调用sync()/msync()/fsync()。
参数设置可以在:/proc/sys/vm下。
其中dirty_writeback_interval实际的参数为:dirty_writeback_centisecs(默认值为500,单位为1/100秒,也就是5秒)
dirty_expire_interval实际的参数为:dirty_expire_centisecs(默认值为3000,单位为1/100秒,也就是30秒)
4.2 执行线程
2.4内核,用一个叫bdflush的线程专门负责writeback操作。因为磁盘I/O操作很慢,而线程操作系统有多个块设备,如果bdflush在其中一个块设备上等待I/O操作的完成,可能会需要很长的时间,此时单线程模式的bdfoush就会成为影响性能的瓶颈。而且bdflush没有周期扫描功能。
在2.6内核中,bdflush和kupdated一起被pdflush(page dirty flush)取代了。pdflush是一组线程,根据块设备的I/O负载情况,数量从最少2个到最多8个不等。如果1秒内没有空闲的pdflush线程,则会创建一个;如果pdflush线程的空闲时间超过1秒,则会被销毁。一个块设备可能有多个可以传输数据的队列,为了避免在队列上的拥塞(congestion),pdflush线程会动态的选择系统中相对空闲的队列。
在2.6.32版本上,直接一个块设备对应一个thread(算法效果不明显),这种内核线程被称为flusher threads。
无论是内核周期性的扫描,还是用户手动触发,flusher threads的write back都是间隔一段时间才进行的。如果这段时间内系统掉电了,那么还没有来得及write back的数据修改就面临丢失的风险,算是page cache机制存在的一个缺点。
5. Buffer和Cache区别
free命令中,存在buff/cache的内容,如下。但是不太好区分。
vbnet
total used free shared buff/cache available
Mem: 19 13 0 0 5 4
Swap: 0 0 0
5.1 free数据来源
通过man free查看指标含义,可以看到buffer和cache的区别。
bash
buffers Memory used by kernel buffers (Buffers in /proc/meminfo)
cache Memory used by the page cache and slabs (Cached and SReclaimable in /proc/meminfo)
buff/cache Sum of buffers and cache
- buffers是内核缓冲区用到的内存,对应的是/proc/meminfo的Buffers值。
- cache是内核也缓存和Slab用到的内存。对应的是/proc/meminfo中的Cached 和 SReclaimable。
通过man proc,可以得到proc文件系统的详细文档,其中就包括了/proc/meminfo的信息
erlang
Buffers %lu Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).
Cached %lu In-memory cache for files read from the disk (the page cache). Doesn't include SwapCached. ...
SReclaimable %lu (since Linux 2.6.19) Part of Slab, that might be reclaimed, such as caches.
SUnreclaim %lu (since Linux 2.6.19) Part of Slab, that cannot be reclaimed on memory pressure.
通过这个文档,可以得到:
- Buffers 是对原始磁盘快的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB左右),这样,内核就可以把分散的写集中起来,统一优化磁盘的写入。
- Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。写词访问这些文件内容时,就可以直接从内存中快速获取,而不需要再次访问缓存的磁盘。
- SReclaimable是Slab的一部分。Slab包括两部分,可回收部分,用SReclaimable记录。不可回收部分,用SUnreclaim记录。
实际上,写文件时会用到Cache缓存数据(虽然文档上,Cache值提到是文件的读缓存),写磁盘则会用到Bufffer来缓存数据。
读文件时,数据会缓存到Cache中。而读磁盘时数据会缓存到Buffer中。
综上:
- Buffer即可以用作"将要写入磁盘数据的缓存",也可以用作"从磁盘读取数据的缓存"。
- Cache既可以用作"从文件读取数据的页缓存",也可以用作"写文件的页缓存"。
参考资料
0)Linux中的内存回收[一] zhuanlan.zhihu.com/p/70964195
1) Linux中的Page Cache [二] zhuanlan.zhihu.com/p/71217136
2)Linux内核中的页面回收算法 liujunming.top/2017/09/28/...
3)Linux内存回收机制 baijiahao.baidu.com/s?id=163211...
4)Linux 内核源码分析-Page Cache 刷脏源码分析 leviathan.vip/2019/06/01/...
5)Page Cache与Page回写 www.cnblogs.com/linhaostudy...
6)极客时间 《LInux内核技术实战课》
7)内存管理参数min_free_kbytes 分析 linux.laoqinren.net/kernel/vm-s...