Page cache(页面缓存)是计算机操作系统中的一种机制,用于将频繁访问的数据从磁盘存储到内存中,以便更快地访问。当程序从磁盘请求数据时,操作系统会检查该数据是否已经存在于页面缓存中。如果存在,数据可以直接从内存中获取,这比从磁盘访问要快得多。如果数据不在页面缓存中,它将从磁盘中获取并存储在缓存中供将来使用。
既然Page Cache是一种缓存,那么缓存必然带来以下问题:
- 占用内存多大空间?
- 内存空间满了怎么办,淘汰策略?
- 内存中的数据何时写入磁盘?
- 数据如何持久化、一致性如何保障?
- 内存中的数据是否会丢失?
- 持久化的触发时机?
page与Page Cache的关系
从磁盘中读取文件后写入Page Cache中,是将整个文件都写入呢,还是只写用到的那一部分?
page是内存管理分配的基本单位,Page Cache由多个page构成。page在操作系统中通常为4KB大小(32bits/64bits),而 Page Cache的大小则为4KB的整数倍。
操作系统为基于Page Cache的读缓存机制提供预读机制(PAGE_READAHEAD),例如用户线程仅仅请求读取磁盘上文件A的offset为0-3KB范围内的数据,由于磁盘的基本读写单位为block(4KB),于是操作系统至少会读0-4KB的内容,这恰好可以在一个page中装下。
但是操作系统出于局部性原理会选择将磁盘块offset[4KB,8KB)、[8KB,12KB)以及[12KB,16KB)都加载到内存,于是额外在内存中申请了3个page。
下面我们用一个例子来演示一下:
准备一个文本文件ooxx.txt,大小为,需大于4*4=16k:
$ ll ooxx.txt
-rw-rw-r-- 1 vagrant vagrant 41211 Dec 29 05:45 ooxx.txt
使用read命令读取ooxx.txt文件的第一行:
$ read a < ooxx.txt
$ echo $a
bash
那么ooxx.txt
前4页的内容将会被缓存在Page Cache中,我们可以使用hcache
命令来查看查询文件或者目录有多少页被载入Page Cache中:
$ hcache ooxx.txt
+----------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|----------+----------------+------------+-----------+---------|
| ooxx.txt | 41211 | 11 | 4 | 036.364 |
+----------+----------------+------------+-----------+---------+
我们可以看到实际上内核使用readahead机制完成了16KB数据的读取,并放入了Page Cache中。
缺页中断
磁盘的数据怎么写入Page Cache?
操作系统以page为单位管理内存,当进程发现需要访问的数据不在内存时,操作系统可能会将数据以页的方式加载到内存中。上述过程被称为缺页中断,当操作系统发生缺页中断时,就会通过系统调用将page再次读到内存中。
缺页中断的作用
缺页中断是内存管理中的重要机制,具有以下重要作用:
-
页面调入内存:当应用程序访问的页面不在内存中时,缺页中断会触发操作系统将缺失的页面从磁盘加载到内存中。这样做的好处是,不需要将整个文件或数据集一次性全部加载到内存中,而是按需加载,只加载应用程序实际需要的页面。这样可以节省磁盘I/O开销,避免了无谓的磁盘读取操作,提高了系统的性能。
-
页面替换策略:当内存中的页面已满时,如果应用程序访问一个新的页面,操作系统需要选择一个页面进行替换,将其换出到磁盘上,为新的页面腾出空间。缺页中断提供了一个时机,让操作系统可以根据页面替换算法选择合适的页面进行替换,以保证内存中的页面是最有用的页面,提高内存的利用率。
-
延迟写策略:在写回策略下,当应用程序对一个脏页进行写操作时,脏页的写回操作不会立即进行,而是推迟到后续的某个时刻进行。这样可以将多次写操作合并为一次写操作,减少了磁盘写入的次数,降低了磁盘I/O开销。
-
页面预读取:为了进一步减少磁盘I/O开销,操作系统可以利用缺页中断的时机进行页面预读取。当应用程序访问一个页面时,操作系统可以预先加载一些相邻的页面到内存中,以提前满足应用程序的访问需求。这样可以减少后续的缺页中断和磁盘读取操作,提高了系统的响应速度。
-
权限检查和处理:缺页中断还可以用于检查应用程序对页面的访问权限。操作系统可以根据页面的访问权限(例如只读、读写等)来判断应用程序的访问是否合法,并根据需要进行相应的处理。
缺页中断的处理流程
当发生缺页中断时,操作系统会按照以下步骤来处理:
-
中断处理程序:当发生缺页中断时,CPU会暂停当前应用程序的执行,并跳转到操作系统的中断处理程序。这个中断处理程序负责处理缺页中断。
-
中断处理程序的执行:中断处理程序会首先保存当前应用程序的上下文信息,包括寄存器的值和程序计数器等。然后,它会根据缺页中断的原因进行相应的处理。
-
页面调入内存:如果缺页中断是由于应用程序访问的页面不在内存中引起的,操作系统会触发页面调入内存的过程。它会根据页面表中的信息,确定需要调入的页面的位置和大小,并从磁盘上读取相应的页面数据。
-
磁盘I/O操作:在页面调入内存的过程中,操作系统会进行磁盘I/O操作,将需要的页面数据从磁盘读取到内存中。这包括了磁盘寻道、读取数据和数据传输等操作。
-
更新页表:当页面数据成功加载到内存中后,操作系统会更新页表,将该页面的状态设置为已加载,并将页面的物理地址映射到对应的虚拟地址。
-
恢复上下文和重新执行应用程序:当页面调入内存完成后,中断处理程序会恢复之前保存的应用程序的上下文信息,并将控制权返回给应用程序。应用程序会继续执行之前被中断的指令,但这次访问的页面已经在内存中了,不会再触发缺页中断。
Page Cache的工作原理
Page Cache的工作原理如下:
-
当应用程序打开一个文件并读取其中的数据时,操作系统会将文件的内容读取到内存中,并将其缓存为一个或多个页(通常是4KB大小的页)。
-
这些缓存的页被存储在一个被称为Page Cache的内存区域中,它是内核管理的一部分。
-
当应用程序再次访问相同的文件时,操作系统首先检查Page Cache中是否存在这些页的副本。如果存在,操作系统会直接从Page Cache中返回数据,而不需要再次访问磁盘。
-
如果应用程序对文件进行写操作,数据会被写入到Page Cache中的脏页(dirty page)。脏页表示该页的内容已经被修改,但还没有写回到磁盘。
-
当系统内存压力较大或需要释放内存时,内核会触发缓存刷新(cache flushing)操作,将脏页从Page Cache写回到磁盘中。
Page Cache的回写策略
回写策略是为了确保缓存数据的一致性和避免数据丢失而设计的。当Page被标记为dirty时,表示该Page中的数据已经被修改过,这些数据需要被写回到磁盘上的,Linux提供了以下两种回写策略:
-
Write Through(写穿):向用户层提供特定接口,应用程序可主动调用接口来保证文件一致性;
-
Write back(写回):系统中存在定期任务(表现形式为内核线程),周期性地同步文件系统中文件脏数据块,这是默认的 Linux一致性方案;
Write Through写穿
Write Through(写透)是将数据写入Page Cache后,立即将数据写入磁盘设备。这样可以确保数据在发生故障或系统崩溃时不会丢失,因为数据已经被持久地写入磁盘。
在JAVA中使用如下的代码实现Write Through写穿:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class WriteThroughTest {
public static void main(String[] args) throws IOException {
File file = new File("/home/vagrant/testfileio/ooxx.txt");
FileOutputStream out = new FileOutputStream(file);
out.write("hello".getBytes(StandardCharsets.UTF_8));
out.getFD().sync(); // 立即刷入磁盘
}
}
我们可以使用strace命令追踪上面程序执行过程中产生的系统调用:
openat(AT_FDCWD, "/home/vagrant/testfileio/ooxx.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(4, "hello", 5) = 5
fsync(4) = 0
close(4)
可以系统调用中包含fsync
,该命令会把文件描述符fd引用的文件修改过的元数据和数据立即写回到存储设备。
Write back写回
在Write Back模式下,写入操作只会将数据写入Page cache,并不立即写入磁盘设备。而是在后续的时间点或特定条件下,将脏页(已被修改但尚未写入磁盘)批量写回磁盘。
在JAVA中使用Write back写回只需要将上面例子中的立即刷入磁盘代码注释即可:
// out.getFD().sync()
这种回写策略下,数据的一致性完全依赖操作系统的自身机制,那么操作系统何时将Page Cache中的脏页数据写入磁盘中呢?
Page Cache写回时机
Linux内核提供了以下参数来优化Page Cache的性能和行为,我们可以通过这些参数来窥探Page Cache的写回时机。
$ sudo sysctl -a | grep "dirty"
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.dirtytime_expire_seconds = 43200
具体参数说明:
-
vm.dirty_background_bytes:设置了系统内存中可以保持脏数据的最大字节数。当系统内存中的脏数据超过这个值时,Linux会开始触发后台刷新(异步刷新)将脏数据写入磁盘。
-
vm.dirty_background_ratio:设置了系统内存中可以保持脏数据的最大比例,默认为10%。
-
vm.dirty_bytes:设置了系统内存中允许累积的脏数据的最大字节数。当脏数据超过这个值时,Linux会触发前台刷新(同步刷新),直到将脏数据写入磁盘为止。
-
vm.dirty_ratio:设置了系统内存中允许累积的脏数据的最大比例,默认为20%。
-
vm.dirty_expire_centisecs:该参数指定了脏数据在内存中能够存活的时间,单位为百分之一秒。当脏数据在内存中超过这个时间后,系统会将其异步写入磁盘中,默认值为3000(30秒)。
-
vm.dirty_writeback_centisecs:表示系统在多长时间内进行一次脏数据的后台写回操作。它的单位是百分之一秒(centiseconds),默认值为500,即系统每5秒钟进行一次后台写回操作。
-
vm.dirtytime_expire_seconds:代表内存中脏数据的允许存储时间,单位为秒。当脏数据在内存中存储的时间超过这个时间,系统会将其写入磁盘,以释放内存。
跟pagecache相关的系统调用
内核提供了下面这些把Page Cache中内容写入磁盘的系统调用:
-
sync:将所有未写的系统缓冲区数据写入磁盘,不需要带任何参数。
-
syncfs:syncfs需要一个文件描述符,只将文件描述符指向的文件相关的文件系统的缓冲区数据写入磁盘。
-
fsync:将文件描述符fd引用的文件修改过的元数据和数据写入磁盘。
-
fdatasync:fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
手动清除Page Cache
$ sync && echo 1 > /proc/sys/vm/drop_caches
表示先执行sync命令,将所有缓存数据写入磁盘中,然后将“1”写入到/proc/sys/vm/drop_caches
文件中,表示清空页缓存。
$ sync && echo 2 > /proc/sys/vm/drop_caches
表示先执行sync命令,将所有缓存数据写入磁盘中,然后将“2”写入到/proc/sys/vm/drop_caches
文件中,表示清空目录缓存和页缓存。
$ sync && echo 3 > /proc/sys/vm/drop_caches
表示先执行sync命令,将所有缓存数据写入磁盘中,然后将“3”写入到/proc/sys/vm/drop_caches
文件中,表示清空目录缓存、页缓存和inode缓存。
hcache工具的使用
hcache是一个用于分析和显示Page Cache统计信息的命令行工具。它可以提供关于Page Cache的详细信息,包括脏页和干净页的数量、Page Cache的大小、缓存命中率等。
hcache是基于pcstat的,pcstat可以查看某个文件是否被缓存和根据进程pid来查看都缓存了哪些文件。hcache在其基础上增加了查看整个操作系统Cache和根据使用Cache大小排序的特性。
安装:
$ wget https://silenceshell-1255345740.cos.ap-shanghai.myqcloud.com/hcache
$ chmod 755 hcache && mv hcache /usr/local/bin/
根据进程pid来查看都缓存了哪些文件
$ hcache -pid $$
+-----------------------------------------------------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|-----------------------------------------------------+----------------+------------+-----------+---------|
| /lib/x86_64-linux-gnu/libtinfo.so.5.9 | 170784 | 42 | 42 | 100.000 |
| /lib/x86_64-linux-gnu/ld-2.27.so | 179152 | 44 | 44 | 100.000 |
| /lib/x86_64-linux-gnu/libnss_compat-2.27.so | 39744 | 10 | 10 | 100.000 |
| /lib/x86_64-linux-gnu/libnss_files-2.27.so | 47568 | 12 | 12 | 100.000 |
| /lib/x86_64-linux-gnu/libnsl-2.27.so | 97176 | 24 | 24 | 100.000 |
| /lib/x86_64-linux-gnu/libnss_nis-2.27.so | 47576 | 12 | 12 | 100.000 |
| /usr/lib/locale/locale-archive | 3004224 | 734 | 734 | 100.000 |
| /lib/x86_64-linux-gnu/libc-2.27.so | 2030928 | 496 | 496 | 100.000 |
| /lib/x86_64-linux-gnu/libdl-2.27.so | 14560 | 4 | 4 | 100.000 |
| /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache | 26376 | 7 | 7 | 100.000 |
| /bin/bash | 1113504 | 272 | 272 | 100.000 |
+-----------------------------------------------------+----------------+------------+-----------+---------+
hcache命令可以帮助您深入了解系统的Page Cache使用情况,以便进行性能分析和优化。