1.什么是PageCache
(1)假如没有PageCache:
CPU如果要访问外部磁盘上的文件,由于cpu可以直接访问的存储器是内存。所以磁盘的文件内容要先拷贝到内存上(DMA技术),cup才能读取到。cup访问内存是很快的高速,内存拷贝磁盘文件相对是慢的。(DMA, Direct Memory Access, 存储器直接访问, 允许在外部设备和存储器之间直接读写数据,既不通过CPU,也不需要CPU干预)
要优化慢这个问题,就需要提前把磁盘数据先读到内存用做缓存。这个在内存上建立的缓存就是PageCache,也叫页缓存。
(2)PageCache作用
有了pageCache,cpu要读的数据如果缓存命中,那速度就会快很多。就类似我们好读mysql的数据,如果提前放到redis上面了,速度就会快得多。
pagecache作用就是:缓存 I/O ,减少读盘的次数,从而提高性能
从前面可以看到,pagecache是放在内存上的,内存同时还有linux内核kernet,app进程这些东西。内存又是有限的,那怎么管理pagecache分配,读取,写入,淘汰就需要有个程序来控制,这个程序就是内核kernet,pagecache的管理是有内核来维护的。
实际上在IO整个过程,其他地方还有缓存的概念
(1)app(应用程序)中的缓存区:buffer
为什么使用buffer会比不使用快?
buffer能一次读取磁盘8k内容先缓存,可以大大减少内核的io次数
Java中BufferedReader,BufferedWriter要比FileReader 和 FileWriter高效
答案就是这里,不用每次读写都调用内核的read/write,而是凑齐8190
字节后再调用一次系统的read/write,本质是较少内核调用磁盘io的次数。
(2)kernel缓存区pagecache
应用程序在调用系统调用如read(),writer(),就会触发中断信号,此时用户态切换到内核态,内核执行完,数据返回了,cup又回来继续执行应用程序。
读数据:缓存磁盘热数据,命中直接返回,较少到磁盘次数
写数据:平衡高速设备和低速设备之间的速度
(3)磁盘上面的磁盘缓存区
磁盘的缓冲区是硬盘与外部总线交换数据的场所。 硬盘的读数据的过程是将磁信号转化为电信号后,通过缓冲区一次次地填充与清空,再填充,再清空,一步步按照PCI总线的周期送出
(1)读cache:
当用户发起一个读请求(假如read()请求),首先操作系统执行中断,从用户态切换到内核态,内核开始调read()方法。内核会先检查目标数据在pagecache中是否有缓存过,有缓存命中(cache hit)直接返回。没有,缓存穿透,去磁盘中读取,然后把目标数据返回并缓存到pagecache。下次需要同样目标数据就可以在缓存中直接读取。
(2)写cache:
用户发起一个写请求(write()),中断到内核。内核会把要写的数据先写入到pagecache.这时内核并不会马上落盘。而且将page标记为dirty(赃页),并将其加入dirty_list中。内核会周期性的将dirty_list的数据落盘(Flusher Threads)。完成了这一步后cache和磁盘中的数据才会最终一致。
落盘策略(Flusher线程群):
1.用户进程调用sync() 和 fsync()系统调用
2.空闲内存低于特定的阈值(threshold)
3.Dirty数据在内存中驻留的时间超过一个特定的阈值
相关参数dity:
sysctl -a | grep dirty vm.dirty_background_bytes = 0 #和 dirty_background_ratio、dirty_ratio 表示同样意义的不同单位的表示 vm.dirty_background_ratio = 5 #表示当脏页占总内存的的百分比超过这个值时,后台线程开始刷新脏页。这个值如果设置得太小,可能不能很好地利用内存加速文件操作。如果设置得太大,则会周期性地出现一个写 IO 的峰值。 vm.dirty_bytes = 0 #和 dirty_background_ratio、dirty_ratio 表示同样意义的不同单位的表示 vm.dirty_expire_centisecs = 3000 #示脏数据多久会被刷新到磁盘上。这里的3000表示 30秒 vm.dirty_ratio = 10 #当脏页占用的内存百分比超过此值时,内核会阻塞掉写操作,并开始刷新脏页 vm.dirty_writeback_centisecs = 500 #表示多久唤醒一次刷新脏页的后台线程。这里的500表示5秒唤醒一次。 |
page cache数量也是有限的,不可能无限增加,那如何释放回收?
(3)cache回收:
内核使用的是LRU算法和Two-List策略(实际上就是怎么回收算法最优的一个机制,其他缓存同样也会面临这个问题,也会有类似的策略)
LRU算法:least rencently used (最近最少使用),就是要释放最近最少使用的
Two-List策略:两个list,实际上是维护两个队列(active:活跃的和inactive:不活跃),两个链表。那很明显要干掉不活跃的(加尾砍头)。那这2个表怎么维护?
1.首次缓存的数据page会加入到inactive list中,inactive list中的page被再次访问,则移入active list.
2.如果active list数量远大于inactive list,那么active list头部的page会被移入inactive list,从而实现两个list平衡
(4)缓存淘汰策略:pageCache VS redis
(1)定义:
一种内存映射文件的方法,mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上(PageCache).
(2)作用:
mmap操作提供了一种机制,让用户程序直接访问设备内存。
什么意思呢,用户空间和内核空间有隔离性,内核才能访问内核空间。用户要直取内核pagecache数据,做不到。需要cup先要从内核的pagecache拷贝一份到用户的缓存区,用户才能访问。有了这个映射之后,用户可以直接操作内核空间的缓存数据。剩一步cup拷贝。这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。
(3)虚拟内存地址和物理地址
虚拟内存地址是连续的
物理内存地址是不连续的。
(4)零拷贝
假设socket进程需要从磁盘读取一份数据,进行网络传输,这里有3种方法可以实现:
传统文件传输,经过两次系统调用,一次是 read()
,一次是 write()
,期间共发生了 4 次用户态与内核态的上下文切换,发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的。
结果 :2次系统调用,4次上下文切换,4次拷贝
有些通路不是必须的,这里我们可以用 mmap()
替换 read()
系统调用函数 ,由于mmap()
也是系统调用函数,可以直接访问pagecache数据,减少一次CPU数据拷贝。但是数据要写到socket缓存区还是需要cup做一次拷贝。
结果 :2次系统调用,4次上下文切换,3次拷贝
从 Linux 内核 2.1 版本开始起,引进一个sendfile()。sendfile() 可以在2个文件描述符之间传递数据(完全在内核中),避免在内核缓冲区和用户缓冲区之间进行数据拷贝,效率很高。可以替代替代前面的 read()
和 write()
这两个系统调用,这样只需一次系统调用。
但是这还不是真正的零拷贝技术。
Linux 内核 2.4开始,又优化了,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程
Block DMA:在传输完一块物理上连续的数据后引起一次中断,然后再由主机进行下一块物理上连续的数据传输
SG-DMA:使用一个链表描述物理上不连续的存储空间。DMA master在传输完一块物理连续的数据后,不用发起中断,而是根据链表来传输下一块物理上连续的数据,直到传输完毕后再发起一次中断.sg-dma有三种工作模式:
Memory-to-Stream即存储接口到流接口
Stream-to-Memory即流接口到存储接口
Memory-to-Memory的存储器到存储器(mmap类似)
第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区(pagecache)里;
第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
结果 :1次系统调用,2次上下文切换,2次拷贝
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
sendfile 只适用于将数据从文件拷贝到 socket 套接字上,同时需要硬件的支持,这也限定了它的使用范围。Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝。splice 的伪代码如下:
splice(fd_in, off_in, fd_out, off_out, len, flags);
splice 系统调用可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作
结果 :1次系统调用,2次上下文切换,2次拷贝
(5)使用零拷贝技术的项目
Kafka:这也是 Kafka 在处理海量数据为什么这么快的原因之一
Nginx:Nginx 支持零拷贝技术,一般默认是开启零拷贝技术,参数:http{... sendfile on; ...}
(6)写时复制
内核缓冲区可能被多个进程所共享,如果某个进程想要这个共享区进行 write 操作,由于 write 不提供任何的锁操作,那么就会对共享区中的数据造成破坏,写时复制的引入就是 Linux 用来保护数据的。写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。
(7)缓冲区共享
缓冲区共享方式完全改写了传统的 I/O 操作,因为传统 I/O 接口都是基于数据拷贝进行的,要避免拷贝就得去掉原先的那套接口并重新改写,所以这种方法是比较全面的零拷贝技术,目前比较成熟的一个方案是在 Solaris 上实现的 fbuf(Fast Buffer,快速缓冲区)。fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到用户空间(user space)和内核态(kernel space),内核和用户共享这个缓冲区池,这样就避免了一系列的拷贝操作。
(8)直接I/O
绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O
(9)直接I/O应用场景
大文件传输(GB级别)
问题:由于文件太大,可能某些部分的文件数据被再次访问的概率比较低,pagecache命中率低,这时发挥不出缓存的优势,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次,反而性能更低;
针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术。
可以使用异步I/O+直接I/O的方案。
传统文件传输,read()之后,会切换到内核态,一直等到:磁盘数据准备好数据,拷贝到pagecache,再由pagacache拷到用户缓存区,read()返回结果才继续。这里read()阻塞问题可以用异常I/O来解决。在read()切换到内核之后,结果可以先返回,继续执行。等磁盘准备发起中断信好,内核将数据直接由磁盘拷贝到用户空间(对于磁盘,异步 I/O 只支持直接 I/O)。
MySQL:
应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启
(10)传统,mmap,sendfile,splice
非超大文件:,mmap ,sendfile,splice 的⽅式要远远优于传统的⽂件拷贝。对于 mmap 和 sendfile/splice 在⽂件较⼩的时候, mmap 耗时更短,当⽂件较⼤时 sendfile/splice 的⽅式最优。
nginx可以根据文件大小来配置参数:
location /video/ { sendfile on; aio on; directio 1024m; }
文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」。