其实,Page Cache 是如何产生和释放的,通俗一点的说就是它的“生”(分配)与“死”(释放),即 Page Cache 的生命周期,那么接下来,我们就先来看一下它是如何“诞生”的。
Page Cache 是如何“诞生”的?
Page Cache 的产生有两种不同的方式:Buffered I/O(标准 I/O);Memory-Mapped I/O(存储映射 I/O)。这两种方式分别都是如何产生 Page Cache 的呢?来看下面这张图:
从图中你可以看到,虽然二者都能产生 Page Cache,但是二者的还是有些差异的:标准 I/O 是写的 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存);如果是读的 (read(2)) 话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容不存在任何映射关系。对于存储映射 I/O 而言,则是直接将 Pagecache Page 给映射到用户地址空间,用户直接读写 Pagecache Page 中内容。显然,存储映射 I/O 要比标准 I/O 效率高一些,毕竟少了“用户空间到内核空间互相拷贝”的过程。这也是很多应用开发者发现,为什么使用内存映射 I/O 比标准 I/O 方式性能要好一些的主要原因。我们来用具体的例子演示一下 Page Cache 是如何“诞生”的,就以其中的标准 I/O 为例,因为这是我们最常使用的一种方式,如下是一个简单的示例脚本:
#!/bin/sh#这是我们用来解析的文件MEM_FILE="/proc/meminfo"#这是在该脚本中将要生成的一个新文件NEW_FILE="/home/yafang/dd.write.out"#我们用来解析的Page Cache的具体项active=0inactive=0pagecache=0IFS=' '#从/proc/meminfo中读取File Page Cache的大小function get_filecache_size(){items=0while read linedoif [[ "$line" =~ "Active:" ]]; thenread -ra ADDR <<<"$line"active=${ADDR[1]}let "items=$items+1"elif [[ "$line" =~ "Inactive:" ]]; thenread -ra ADDR <<<"$line"inactive=${ADDR[1]}let "items=$items+1"fiif [ $items -eq 2 ]; thenbreak;fidone < $MEM_FILE}#读取File Page Cache的初始大小get_filecache_sizelet filecache="$active + $inactive"#写一个新文件,该文件的大小为1048576 KBdd if=/dev/zero of=$NEW_FILE bs=1024 count=1048576 &> /dev/null#文件写完后,再次读取File Page Cache的大小get_filecache_size#两次的差异可以近似为该新文件内容对应的File Page Cache#之所以用近似是因为在运行的过程中也可能会有其他Page Cache产生let size_increased="$active + $inactive - $filecache"#输出结果echo "File size 1048576KB, File Cache increased" $size_increased
通过这个脚本你可以看到,在创建一个文件的过程中,代码中 /proc/meminfo 里的 Active(file) 和 Inactive(file) 这两项会随着文件内容的增加而增加,它们增加的大小跟文件大小是一致的(这里之所以略有不同,是因为系统中还有其他程序在运行)。另外,如果你观察得很仔细的话,你会发现增加的 Page Cache 是 Inactive(File) 这一项,你可以去思考一下为什么会这样?这里就作为咱们这节课的思考题。当然,这个过程看似简单,但是它涉及的内核机制还是很多的,换句话说,可能引起问题的地方还是很多的,我们用一张图简单描述下这个过程:
这个过程大致可以描述为:首先往用户缓冲区 buffer(这是 Userspace Page) 写入数据,然后 buffer 中的数据拷贝到内核缓冲区(这是 Pagecache Page),如果内核缓冲区中还没有这个 Page,就会发生 Page Fault 会去分配一个 Page,拷贝结束后该 Pagecache Page 是一个 Dirty Page(脏页),然后该 Dirty Page 中的内容会同步到磁盘,同步到磁盘后,该 Pagecache Page 变为 Clean Page 并且继续存在系统中。
root@k8s-master01:~/test# cat /proc/vmstat | egrep "dirty|writeback"nr_dirty 12nr_writeback 0nr_writeback_temp 0nr_dirty_threshold 251976nr_dirty_background_threshold 125834root@k8s-master01:~/test#
Page Cache 是如何“死亡”的?
你可以把 Page Cache 的回收行为 (Page Reclaim) 理解为 Page Cache 的“自然死亡”。言归正传,我们知道,服务器运行久了后,系统中 free 的内存会越来越少,用 free 命令来查看,大部分都会是 used 内存或者 buff/cache 内存,比如说下面这台生产环境中服务器的内存使用情况:
root@k8s-master01:~/test# free -gtotal used free shared buff/cache availableMem: 6 1 3 0 1 4Swap: 0 0 0root@k8s-master01:~/test#
free 命令中的 buff/cache 中的这些就是“活着”的 Page Cache,那它们什么时候会“死亡”(被回收)呢?我们来看一张图:
你可以看到,应用在申请内存的时候,即使没有 free 内存,只要还有足够可回收的 Page Cache,就可以通过回收 Page Cache 的方式来申请到内存,回收的方式主要是两种:直接回收和后台回收。那它是具体怎么回收的呢?你要怎么观察呢?其实在我看来,观察 Page Cache 直接回收和后台回收最简单方便的方式是使用 sar:
root@k8s-master01:~/test# sar -B 1Linux 4.15.0-20-generic (k8s-master01) 2021年09月26日 _x86_64_ (2 CPU)10时18分10秒 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff10时18分11秒 0.00 24.00 1474.00 0.00 1189.00 0.00 0.00 0.00 0.0010时18分12秒 0.00 32.00 840.00 0.00 654.00 0.00 0.00 0.00 0.0010时18分13秒 0.00 0.00 1244.00 0.00 1115.00 0.00 0.00 0.00 0.0010时18分14秒 0.00 91.09 715.84 0.00 611.88 0.00 0.00 0.00 0.00^CAverage: 0.00 36.91 1067.58 0.00 891.77 0.00 0.00 0.00 0.00root@k8s-master01:~/test#
借助上面这些指标,你可以更加明确地观察内存回收行为,下面是这些指标的具体含义:
- pgscank/s : kswapd(后台回收线程) 每秒扫描的 page 个数。
- pgscand/s: Application 在内存申请过程中每秒直接扫描的 page 个数。
- pgsteal/s: 扫描的 page 中每秒被回收的个数。
- %vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 说明系统越安全,越接近 0 说明系统内存压力越大。
这几个指标也是通过解析 /proc/vmstat 里面的数据来得出的,对应关系如下:

