如果说想用一句话说明白什么是 Page Cache,那么我感觉这么说比较合适,“Page Cache 是用于加速对磁盘上数据访问的一部分 RAM”。下面是比较学院派的解释。
Page Cache 是 Linux 内核对磁盘最主要的缓存。大多数情况下,内核在读取或写入磁盘时会引用 Page Cache。新页面被添加到 Page Cache 中,以满足用户空间进程的读取请求。如果该页尚未在缓存中,则会将新条目添加到缓存中并用从磁盘读取的数据填充。该页面将尽量久的保留在缓存中,然后可以由其他进程重用,而无需访问磁盘。
类似的,在向磁盘写入页数据之前,内核会验证相应的页面是否已经包含在 Page Cache 中;如果不在,就会向缓存中添加一个新条目,并用要写入磁盘的数据填充。向磁盘写入的 I/O 数据传输不会立即开始:磁盘更新会被延迟几秒钟,从而给进程进一步修改要写入的数据的机会(这里我感觉是为了批量处理更有效率)。
从图中可以看到 Page Cache是内核管理的内存,也就是说,它属于内核而不属于用户。
在 Linux 中有多种方式可以查询 Page Cache,常用的有 free , /proc/meminfo,vmstat 等。
$free -m
total used free shared buff/cache available
Mem: 8192 3639 4058 34 493 4313
Swap: 0 0 0
$cat /proc/meminfo
MemTotal: 8388608 kB
MemFree: 4155612 kB
MemAvailable: 4417412 kB
Buffers: 0 kB
Cached: 506220 kB
SwapCached: 0 kB
Active: 169688 kB
Inactive: 4061128 kB
Active(anon): 528 kB
Inactive(anon): 3706692 kB
Active(file): 169160 kB
Inactive(file): 354436 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 3724116 kB
Mapped: 112992 kB
Shmem: 34848 kB
KReclaimable: 4820560 kB
Slab: 0 kB
SReclaimable: 0 kB
SUnreclaim: 0 kB
KernelStack: 0 kB
PageTables: 648124 kB
... ...
$vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 4154464 0 506748 0 0 1449 10175 2 2 0 0 100 0 0
Page Cache 的产生
从图中还可以得到的信息就是 Page Cache 的产生主要有两种方式:
- Buffered I/O(标准I/O)
- Memory-Mapped I/O(存储映射I/O)
而直接 I/O (Direct I/O) 不会走 Page Cache 将直接与存储打交道。
产生方式的区别
标准 I/O 是先写到用户缓冲区(User Space 对应的内存),然后再将用户缓冲区里的数据拷贝(write(2))到内核缓冲区(Page Cache Page对应的内存);如果是读的(read(2))话,则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容不存在任何映射关系。
对于存储映射 I/O 而言,则是直接将 Page Cache Page 给映射到用户地址空间,用户直接读写Page Cache Page 中内容。
标准I/O
以标准I/O为例,解释一下 Page Cache 是如何产生的。
- 首先往用户缓冲区(buffer)中写入数据,然后调用 write() 方法将 buffer 中的数据拷贝到内核缓冲区(Page Cache Page);
- 如果内核缓冲区中没有这个 Page,就会发生 Page Fault(缺页),将先分配一个 Page;
- 然后执行数据拷贝,该 Page Cache Page 就变成一个 Dirty Page(脏页);
- 最后 Dirty Page 的内容会同步到磁盘,同步到磁盘后,该 Page Cache Page 就会变成 Clean Page 并且继续存在系统中。
Page Cache 的作用
下面我们通过一个例子,来实际验证下 Page Cahe 在文件写入以及读取中的作用。
首先我们用下面的命令来生成一个 100M 大小的文件,并分别观察前后的 buffer/cache 变化,以及 dirty page 的变化情况。
$free -m
total used free shared buff/cache available
Mem: 8192 3415 4450 34 325 4603
Swap: 0 0 0
$dd if=/dev/zero of=testfile.txt bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 0.12828 s, 817 MB/s
$cat /proc/meminfo | grep Dirty
Dirty: 102400 kB
$free -m
total used free shared buff/cache available
Mem: 8192 3416 4348 34 426 4556
Swap: 0 0 0
可以看到 buffer/cache 如期望的增长了 100M,同时看 dirty page 的大小也是 100M。在 dirty page 回写之后(变成0),buffer/cache 还是保持着大小不变,说明 Page Cache 不会随着 ditry page 的回写而消亡。
$time cat testfile.txt &> /dev/null
real 0m0.030s
user 0m0.000s
sys 0m0.030s
然后我们使用读取命令来读取下刚写的文件,由于写入数据时候的 Page Cache 还在,所以现在的读取其实是从 Page Cache 中直接读取的。要看到读取原始文件的效果,需要我们先利用下面的命令,来清空下 Page Cache。
$sudo bash -c 'echo 3 > /proc/sys/vm/drop_caches'
$free -m
total used free shared buff/cache available
Mem: 8192 3419 4476 34 295 4620
Swap: 0 0 0
可以看到 Cache 已经被清理了,让我们再来执行下面的读取命令。
$time cat testfile.txt &> /dev/null
real 0m0.174s
user 0m0.000s
sys 0m0.050s
$free -m
total used free shared buff/cache available
Mem: 8192 3420 4353 34 418 4558
Swap: 0 0 0
$time cat testfile.txt &> /dev/null
real 0m0.038s
user 0m0.001s
sys 0m0.027s
可以看到在清理 Page Cache 之后,读取时长有明显的增加。而再次读取的时候,由于又有了 Page Cache 的加持,时长跟刚写完时差不多。
Page Cache 回收
Page Cache 毕竟是为了提高性能占用的物理内存,随着越来越多的磁盘数据被缓存到内存中,Page Cache 变得越来越大,如果一些重要的任务需要被 Page Cache 占用的内存,内核将回收 Page Cache 以支持这些需求。
回收的方式主要是两种:直接回收和后台回收,具体的回收行为,可以使用以下命令查看:
$sar -B 1
Linux 5.10.112-005.ali5000.al8.x86_64 (collection-bullet033061130171.pre.na610) 06/25/2024 _x86_64_ (104 CPU)
05:51:47 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
05:51:48 PM 1352.00 43852.00 319867.00 3.00 345241.00 0.00 0.00 14.00 0.00
05:51:49 PM 8924.00 28396.00 549915.00 26.00 291152.00 0.00 0.00 0.00 0.00
05:51:50 PM 560.00 35836.00 677473.00 0.00 181327.00 0.00 0.00 0.00 0.00
05:51:51 PM 1024.00 25560.00 468166.00 0.00 285465.00 0.00 0.00 0.00 0.00
05:51:52 PM 1024.00 18540.00 451655.00 0.00 216495.00 0.00 0.00 0.00 0.00
05:51:53 PM 1064.00 14836.00 455541.00 2.00 282231.00 0.00 0.00 2126.00 0.00
05:51:54 PM 896.00 18880.00 334210.00 0.00 162607.00 0.00 0.00 0.00 0.00
05:51:55 PM 1024.00 16580.00 492492.00 0.00 162693.00 0.00 0.00 0.00 0.00
05:51:56 PM 1024.00 21184.00 562577.00 0.00 275626.00 0.00 0.00 0.00 0.00
05:51:56 PM 775.76 29745.45 307830.30 0.00 190100.00 0.00 0.00 0.00 0.00
Average: 1837.94 25024.65 473041.80 3.32 242826.37 0.00 0.00 229.37 0.00
- pgscank/s : kswapd(后台回收线程) 每秒扫描的 Page 个数。
- pgscand/s: Application 在内存申请过程中每秒直接扫描的 Page 个数。
- pgsteal/s: 扫描的 Page 中每秒被回收的个数。
- %vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 说明系统越安全,越接近 0 说明系统内存压力越大。
触发条件
1.空间层面
当系统中 dirty 的内存大于某个阈值时。该阈值用在 dirtyable memory 中的占比 dirty_background_ratio(默认为10%),或者绝对的字节数 dirty_background_bytes(2.6.29内核引入)来指定。如果两者同时设置的话,那么以 bytes 为更高优先级。
此外,还有 dirty_ratio(默认为20%)和 dirty_bytes,它们的意思是当 dirty 的内存达到这个数量(屋里太脏),进程自己都看不过去了,宁愿停下手头的 write 操作(被阻塞,同步),先去把这些 dirty 的 writeback 了(把屋里打扫干净)。
而如果 dirty 的程度介于 dirty_ratio 和 dirty_background_ratio 之间(10% - 20%),就交给后面要介绍的专门负责 writeback 的 background 线程去做就好了(专职的清洁工,异步)。
2.时间层面
通过周期性的扫描来发现需要回收的 dirty page,扫描间隔用参数:dirty_writeback_interval 控制。发现存在最近一次更新时间超过某个阈值(参数:dirty_expire_interval)的 Page。如果每个 Page 都维护最近更新时间,开销会很大且扫描会很耗时,因此具体实现不会以 Page 为粒度,而是按 inode 中记录的 dirtying-time 来计算。
dirty_writeback_interval 实际的参数为:dirty_writeback_centisecs(默认值为 500,单位为 1/100 秒,也就是 5 秒)
dirty_expire_interval 实际的参数为:dirty_expire_centisecs(默认值为 3000,单位为 1/100 秒,也就是 30 秒)
3.用户主动发起。
调用 sync() / msync() / fsync()。
执行线程
在 2.4 内核,用一个叫 bdflush 的线程专门负责 writeback 操作。因为磁盘 I/O 操作很慢,而操作系统有多个块设备,如果 bdflush 在其中一个块设备上等待 I/O 操作的完成,可能会需要很长的时间,此时单线程模式的 bdflush 就会成为影响性能的瓶颈。而且 bdflush 没有周期扫描功能。
在2.6 内核中,bdflush 和 kupdated 一起被 pdflush(page dirty flush)取代了。pdflush 是一组线程,根据块设备的 I/O 负载情况,数量从最少 2 个到最多 8 个不等。如果 1 秒内没有空闲的 pdflush 线程,则会创建一个;如果 pdflush 线程的空闲时间超过 1 秒,则会被销毁。一个块设备可能有多个可以传输数据的队列,为了避免在队列上的拥塞,pdflush 线程会动态的选择系统中相对空闲的队列。
这种方法在理论上是很优秀的,然而现实的情况是外部 I/O 和 CPU 的速度差异巨大,但 I/O 系统的其他部分并没有都使用拥塞控制,因此 pdflush 单独使用复杂的拥塞算法的效果并不明显,可以说是“独木难支”。
于是在2.6.32 内核中,干脆化繁为简,直接一个块设备对应一个 thread,这种内核线程被称为 flusher threads ,线程名为“writeback”,执行体为“wb_workfn”,通过 workqueue 机制实现调度。
无论是内核周期性扫描,还是用户手动触发,flusher threads 的 writeback 都是间隔一段时间才进行的,如果在这段时间内系统掉电了(power failure),那还没来得及 writeback 的数据修改就面临丢失的风险,这是 Page Cache 机制存在的一个缺点。writeback 越频繁,数据因意外丢失的风险越低,但同时 I/O 压力也越大。技术本来就是这样,没有完美的方案,只有最好的方案。
清理 Page Cache
- 运行 sync() 将 dirty 的内容写回硬盘
- 通过修改 proc 系统的 drop_caches 清理 free 的 cache
可以通过 /proc/vmstat 文件判断是否执行过 drop_caches:
$cat /proc/vmstat | grep drop
drop_pagecache 2
drop_slab 2
Page Cache 重要配置参数
在/proc/sys/vm中有以下文件与回写 ditry page 密切相关:
配置文件 | 功能 | 默认值 |
---|---|---|
dirty_background_ratio | 触发回刷的脏数据占可用内存的百分比 | 0 |
dirty_background_bytes | 触发回刷的脏数据量 | 10 |
dirty_bytes | 触发同步写的脏数据量 | 0 |
dirty_ratio | 触发同步写的脏数据占可用内存的百分比 | 20 |
dirty_expire_centisecs | 脏数据超时回刷时间(单位:1/100s) | 3000 |
dirty_writeback_centisecs | 回刷进程定时唤醒时间(单位:1/100s) | 500 |
/proc/sys/vm/dirty_ratio(同步刷盘)
这个参数控制文件系统的文件系统写缓冲区的大小,单位是百分比,表示系统内存的百分比,
表示当写缓冲使用到系统内存多少的时候,开始向磁盘写出数据。
增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。
但是,当你需要持续、恒定的写入场合时,应该降低其数值,一般启动上缺省是 20。
/proc/sys/vm/dirty_background_ratio(异步刷盘)
这个参数控制文件系统的 pdflush 进程,在何时刷新磁盘。
单位是百分比,表示系统内存的百分比,意思是当写缓冲使用到系统内存多少的时候,pdflush 开始向磁盘写出数据。
增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。
但是,当你需要持续、恒定的写入场合时, 应该降低其数值,一般启动上缺省是 10
/proc/sys/vm/dirty_writeback_centisecs
这个参数控制内核的脏数据刷新进程 pdflush 的运行间隔。
单位是 1/100 秒。缺省数值是500,也就是 5 秒。
如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操。
/proc/sys/vm/dirty_expire_centisecs
这个参数声明 Linux 内核写缓冲区里面的数据多“旧”了之后,pdflush 进程就开始考虑写到磁盘中去。
单位是 1/100秒。缺省是 3000,也就是 30 秒的数据就算旧了,将会刷新磁盘。
对于特别重载的写操作来说,这个值适当缩小也是好的,但也不能缩小太多,因为缩小太多也会导致 IO 提高太快。