精华内容
下载资源
问答
  • pageCache
    千次阅读
    2019-03-17 19:19:40

    写在前面

    在开始正式的讨论前,我先抛出几个问题:

    • 谈到磁盘时,常说的HDD磁盘和SSD磁盘最大的区别是什么?这些差异会影响我们的系统设计吗?
    • 单线程写文件有点慢,那多开几个线程一起写是不是可以加速呢?
    • write(2)函数成功返回了,数据就已经成功写入磁盘了吗?此时设备断电会有影响吗?会丢失数据吗?
    • write(2)调用是原子的吗?多线程写文件是否要对文件加锁?有没有例外,比如O_APPEND方式?
    • 坊间传闻,mmap(2)的方式读文件比传统的方式要快,因为少一次拷贝。真是这样吗?为什么少一次拷贝?

    如果你觉得这些问题都很简单,都能很明确的回答上来。那么很遗憾这篇文章不是为你准备的,你可以关掉网页去做其他更有意义的事情了。如果你觉得无法明确的回答这些问题,那么就耐心地读完这篇文章,相信不会浪费你的时间。受限于个人时间和文章篇幅,部分议题如果我不能给出更好的解释或者已有专业和严谨的资料,就只会给出相关的参考文献的链接,请读者自行参阅。

    言归正传,我们的讨论从存储器的层次结构开始。

    存储器的金字塔结构

    受限于存储介质的存取速率和成本,现代计算机的存储结构呈现为金字塔型[1]。越往塔顶,存取效率越高、但成本也越高,所以容量也就越小。得益于程序访问的局部性原理[2],这种节省成本的做法也能取得不俗的运行效率。从存储器的层次结构以及计算机对数据的处理方式来看,上层一般作为下层的Cache层来使用(广义上的Cache)。比如寄存器缓存CPU Cache的数据,CPU Cache L1~L3层视具体实现彼此缓存或直接缓存内存的数据,而内存往往缓存来自本地磁盘的数据。

    本文主要讨论磁盘IO操作,故只聚焦于Local Disk的访问特性和其与DRAM之间的数据交互。

    无处不在的缓存

    如图,当程序调用各类文件操作函数后,用户数据(User Data)到达磁盘(Disk)的流程如图所示[3]。图中描述了Linux下文件操作函数的层级关系和内存缓存层的存在位置。中间的黑色实线是用户态和内核态的分界线。

    从上往下分析这张图,首先是C语言stdio库定义的相关文件操作函数,这些都是用户态实现的跨平台封装函数。stdio中实现的文件操作函数有自己的stdio buffer,这是在用户态实现的缓存。此处使用缓存的原因很简单——系统调用总是昂贵的。如果用户代码以较小的size不断的读或写文件的话,stdio库将多次的读或者写操作通过buffer进行聚合是可以提高程序运行效率的。stdio库同时也支持fflush(3)函数来主动的刷新buffer,主动的调用底层的系统调用立即更新buffer里的数据。特别地,setbuf(3)函数可以对stdio库的用户态buffer进行设置,甚至取消buffer的使用。

    系统调用的read(2)/write(2)和真实的磁盘读写之间也存在一层buffer,这里用术语Kernel buffer cache来指代这一层缓存。在Linux下,文件的缓存习惯性的称之为Page Cache,而更低一级的设备的缓存称之为Buffer Cache. 这两个概念很容易混淆,这里简单的介绍下概念上的区别Page Cache用于缓存文件的内容,和文件系统比较相关。文件的内容需要映射到实际的物理磁盘,这种映射关系由文件系统来完成;Buffer Cache用于缓存存储设备块(比如磁盘扇区)的数据,而不关心是否有文件系统的存在(文件系统的元数据缓存在Buffer Cache中)。

    综上,既然讨论Linux下的IO操作,自然是跳过stdio库的用户态这一堆东西,直接讨论系统调用层面的概念了。对stdio库的IO层有兴趣的同学可以自行去了解。从上文的描述中也介绍了文件的内核级缓存是保存在文件系统的Page Cache中的。所以后面的讨论基本上是讨论IO相关的系统调用和文件系统Page Cache的一些机制。

    Linux内核中的IO栈

    这一小节来看Linux内核的IO栈的结构。先上一张全貌图[4]:

    由图可见,从系统调用的接口再往下,Linux下的IO栈致大致有三个层次:

    1. 文件系统层,以 write(2) 为例,内核拷贝了write(2)参数指定的用户态数据到文件系统Cache中,并适时向下层同步
    2. 块层,管理块设备的IO队列,对IO请求进行合并、排序(还记得操作系统课程学习过的IO调度算法吗?)
    3. 设备层,通过DMA与内存直接交互,完成数据和具体设备之间的交互

    结合这个图,想想Linux系统编程里用到的Buffered IOmmap(2)Direct IO,这些机制怎么和Linux IO栈联系起来呢?上面的图有点复杂,我画一幅简图,把这些机制所在的位置添加进去:

    这下一目了然了吧?传统的Buffered IO使用read(2)读取文件的过程什么样的?假设要去读一个冷文件(Cache中不存在),open(2)打开文件内核后建立了一系列的数据结构,接下来调用read(2),到达文件系统这一层,发现Page Cache中不存在该位置的磁盘映射,然后创建相应的Page Cache并和相关的扇区关联。然后请求继续到达块设备层,在IO队列里排队,接受一系列的调度后到达设备驱动层,此时一般使用DMA方式读取相应的磁盘扇区到Cache中,然后read(2)拷贝数据到用户提供的用户态buffer中去(read(2)的参数指出的)。

    整个过程有几次拷贝?从磁盘到Page Cache算第一次的话,从Page Cache到用户态buffer就是第二次了。而mmap(2)做了什么?mmap(2)直接把Page Cache映射到了用户态的地址空间里了,所以mmap(2)的方式读文件是没有第二次拷贝过程的。那Direct IO做了什么?这个机制更狠,直接让用户态和块IO层对接,直接放弃Page Cache,从磁盘直接和用户态拷贝数据。好处是什么?写操作直接映射进程的buffer到磁盘扇区,以DMA的方式传输数据,减少了原本需要到Page Cache层的一次拷贝,提升了写的效率。对于读而言,第一次肯定也是快于传统的方式的,但是之后的读就不如传统方式了(当然也可以在用户态自己做Cache,有些商用数据库就是这么做的)。

    除了传统的Buffered IO可以比较自由的用偏移+长度的方式读写文件之外,mmap(2)Direct IO均有数据按页对齐的要求,Direct IO还限制读写必须是底层存储设备块大小的整数倍(甚至Linux 2.4还要求是文件系统逻辑块的整数倍)。所以接口越来越底层,换来表面上的效率提升的背后,需要在应用程序这一层做更多的事情。所以想用好这些高级特性,除了深刻理解其背后的机制之外,也要在系统设计上下一番功夫。

    Page Cache 的同步

    广义上Cache的同步方式有两种,即Write Through(写穿)Write back(写回). 从名字上就能看出这两种方式都是从写操作的不同处理方式引出的概念(纯读的话就不存在Cache一致性了,不是么)。对应到Linux的Page Cache上所谓Write Through就是指write(2)操作将数据拷贝到Page Cache后立即和下层进行同步的写操作,完成下层的更新后才返回。而Write back正好相反,指的是写完Page Cache就可以返回了。Page Cache到下层的更新操作是异步进行的。

    Linux下Buffered IO默认使用的是Write back机制,即文件操作的写只写到Page Cache就返回,之后Page Cache到磁盘的更新操作是异步进行的。Page Cache中被修改的内存页称之为脏页(Dirty Page),脏页在特定的时候被一个叫做pdflush(Page Dirty Flush)的内核线程写入磁盘,写入的时机和条件如下:

    • 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。
    • 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘。
    • 用户进程调用sync(2)fsync(2)fdatasync(2)系统调用时,内核会执行相应的写回操作。

    刷新策略由以下几个参数决定(数值单位均为1/100秒):

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

     

    # flush每隔5秒执行一次

    root@082caa3dfb1d / $ sysctl vm.dirty_writeback_centisecs

    vm.dirty_writeback_centisecs = 500

    # 内存中驻留30秒以上的脏数据将由flush在下一次执行时写入磁盘

    root@082caa3dfb1d / $ sysctl vm.dirty_expire_centisecs

    vm.dirty_expire_centisecs = 3000

    # 若脏页占总物理内存10%以上,则触发flush把脏数据写回磁盘

    root@082caa3dfb1d / $ sysctl vm.dirty_background_ratio

    vm.dirty_background_ratio = 10

    默认是写回方式,如果想指定某个文件是写穿方式呢?即写操作的可靠性压倒效率的时候,能否做到呢?当然能,除了之前提到的fsync(2)之类的系统调用外,在open(2)打开文件时,传入O_SYNC这个flag即可实现。这里给篇参考文章[5],不再赘述(更好的选择是去读TLPI相关章节)。

    文件读写遭遇断电时,数据还安全吗?相信你有自己的答案了。使用O_SYNC或者fsync(2)刷新文件就能保证安全吗?现代磁盘一般都内置了缓存,代码层面上也只能讲数据刷新到磁盘的缓存了。当数据已经进入到磁盘的高速缓存时断电了会怎么样?这个恐怕不能一概而论了。不过可以使用hdparm -W0命令关掉这个缓存,相应的,磁盘性能必然会降低。

    文件操作与锁

    当多个进程/线程对同一个文件发生写操作的时候会发生什么?如果写的是文件的同一个位置呢?这个问题讨论起来有点复杂了。首先write(2)调用不是原子操作,不要被TLPI的中文版5.2章节的第一句话误导了(英文版也是有歧义的,作者在这里给出了勘误信息)。当多个write(2)操作对一个文件的同一部分发起写操作的时候,情况实际上和多个线程访问共享的变量没有什么区别。按照不同的逻辑执行流,会有很多种可能的结果。也许大多数情况下符合预期,但是本质上这样的代码是不可靠的。

    特别的,文件操作中有两个操作是内核保证原子的。分别是open(2)调用的O_CREATO_APPEND这两个flag属性。前者是文件不存在就创建,后者是每次写文件时把文件游标移动到文件最后追加写(NFS等文件系统不保证这个flag)。有意思的问题来了,以O_APPEND方式打开的文件write(2)操作是不是原子的?文件游标的移动和调用写操作是原子的,那写操作本身会不会发生改变呢?有的开源软件比如apache写日志就是这样写的,这是可靠安全的吗?坦白讲我也不清楚,有人说Then O_APPEND is atomic and write-in-full for all reasonably-sized> writes to regular files.但是我也没有找到很权威的说法。这里给出一个邮件列表上的讨论,可以参考下[6]。今天先放过去,后面有时间的话专门研究下这个问题。如果你能给出很明确的说法和证明,还望不吝赐教。

    Linux下的文件锁有两种,分别是flock(2)的方式和fcntl(2)的方式,前者源于BSD,后者源于System V,各有限制和应用场景。老规矩,TLPI上讲的很清楚的这里不赘述。我个人是没有用过文件锁的,系统设计的时候一般会避免多个执行流写一个文件的情况,或者在代码逻辑上以mutex加锁,而不是直接加锁文件本身。数据库场景下这样的操作可能会多一些(这个纯属臆测),这就不是我了解的范畴了。

    磁盘的性能测试

    在具体的机器上跑服务程序,如果涉及大量IO的话,首先要对机器本身的磁盘性能有明确的了解,包括不限于IOPS、IO Depth等等。这些数据不仅能指导系统设计,也能帮助资源规划以及定位系统瓶颈。比如我们知道机械磁盘的连续读写性能一般不会超过120M/s,而普通的SSD磁盘随意就能超过机械盘几倍(商用SSD的连续读写速率达到2G+/s不是什么新鲜事)。另外由于磁盘的工作原理不同,机械磁盘需要旋转来寻找数据存放的磁道,所以其随机存取的效率受到了“寻道时间”的严重影响,远远小于连续存取的效率;而SSD磁盘读写任意扇区可以认为是相同的时间,随机存取的性能远远超过机械盘。所以呢,在机械磁盘作为底层存储时,如果一个线程写文件很慢的话,多个线程分别去写这个文件的各个部分能否加速呢?不见得吧?如果这个文件很大,各个部分的寻道时间带来极大的时间消耗的话,效率就很低了(先不考虑Page Cache)。SSD呢?可以明确,设计合理的话,SSD多线程读写文件的效率会高于单线程。当前的SSD盘很多都以高并发的读取为卖点的,一个线程压根就喂不饱一块SSD盘。一般SSD的IO Depth都在32甚至更高,使用32或者64个线程才能跑满一个SSD磁盘的带宽(同步IO情况下)。

    具体的SSD原理不在本文计划内,这里给出一篇详细的参考文章[7]。有时候一些文章中所谓的SATA磁盘一般说的就是机械盘(虽然SATA本身只是一个总线接口)。接口会影响存储设备的最大速率,基本上是SATA -> PCI-E -> NVMe的发展路径,具体请自行Google了解。

    具体的设备一般使用fio工具[8]来测试相关磁盘的读写性能。fio的介绍和使用教程有很多[9],不再赘述。这里不想贴性能数据的原因是存储介质的发展实在太快了,一方面不想贴某些很快就过时的数据以免让初学者留下不恰当的第一印象,另一方面也希望读写自己实践下fio命令。

    前文提到存储介质的原理会影响程序设计,我想稍微的解释下。这里说的“影响”不是说具体的读写能到某个速率,程序中就依赖这个数值,换个工作环境就性能大幅度降低(当然,为专门的机型做过优化的结果很可能有这个副作用)。而是说根据存储介质的特性,程序的设计起码要遵循某个设计套路。举个简单的例子,SATA机械盘的随机存取很慢,那系统设计时,就要尽可能的避免随机的IO出现,尽可能的转换成连续的文件存取来加速运行。比如Google的LevelDB就是转换随机的Key-Value写入为Binlog(连续文件写入)+ 内存插入MemTable(内存随机读写可以认为是O(1)的性能),之后批量dump到磁盘(连续文件写入)。这种LSM-Tree的设计便是合理的利用了存储介质的特性,做到了最大化的性能利用(磁盘换成SSD也依旧能有很好的运行效率)。

    写在最后

    每天抽出不到半个小时,零零散散地写了一周,这是说是入门都有些谬赞了,只算是对Linux下的IO机制稍微深入的介绍了一点。无论如何,希望学习完Linux系统编程的同学,能继续的往下走一走,尝试理解系统调用背后隐含的机制和原理。探索的结果无所谓,重要的是探索的过程以及相关的学习经验和方法。前文提出的几个问题我并没有刻意去解答所有的,但是读到现在,不知道你自己能回答上几个了?

     

    参考文献

    [1] 图片引自《Computer Systems: A Programmer’s Perspective》Chapter 6 The Memory Hierarchy, 另可参考https://zh.wikipedia.org/wiki/%E5%AD%98%E5%82%A8%E5%99%A8%E5%B1%B1

    [2] Locality of reference,https://en.wikipedia.org/wiki/Locality_of_reference

    [3] 图片引自《The Linux Programming Interface》Chapter 13 FILE I/O BUFFERING

    [4] Linux Storage Stack Diagram, https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram

    [5] O_DIRECT和O_SYNC详解, http://www.cnblogs.com/suzhou/p/5381738.html

    [6] http://librelist.com/browser/usp.ruby/2013/6/5/o-append-atomicity/

    [7] Coding for SSD, https://dirtysalt.github.io/coding-for-ssd.html

    [8] fio作者Jens Axboe是Linux内核IO部分的maintainer,工具主页 http://freecode.com/projects/fio/

    [9] How to benchmark disk, https://www.binarylane.com.au/support/solutions/articles/1000055889-how-to-benchmark-disk-i-o

    [10] 深入Linux内核架构, (德)莫尔勒, 人民邮电出版社

    更多相关内容
  • 实验环境 CentOS Linux release 7.3.1611 (Core) 3.10.0-514.6.1.el7.x86_64  一、概念介绍 ...  used:已使用的内存(total – free – buffers – cache)  free:未使用的内存  shared:通常情况下是t
  • linux那些事之page cache

    千次阅读 2022-03-04 20:00:45
    page cache page cache又称高速缓存,主要是针对文件文件系统,为了减少不必要的磁盘IO操作(读/写)造成卡顿问题,内核将磁盘文件中的内容缓存到内存中,并选择适当时机对磁盘进行读写操作。 page cache在linux...

    page cache

    page cache又称高速缓存,主要是针对文件文件系统,为了减少不必要的磁盘IO操作(读/写)造成卡顿问题,内核将磁盘文件中的内容缓存到内存中,并选择适当时机对磁盘进行读写操作。

     page cache在linux文件系统中占用重要地位,作为文件缓冲。由于CPU访问磁盘比从内存中读取要慢上百甚至上千倍;同时利用空间和时间局部性,当系统访问该文件中一定位置的内容之后,那么经常会很快访问附近位置,如果能提前将文件一部分内容缓存到内存中将会加快对文件操作效率。


    the Linux kernel implements a disk cache called the page cache.The goal of this cache is to minimize disk I/O by storing data in physical memory that would otherwise require disk access.
    Two factors comingle to make disk caches a critical component of any modern operating system. First, disk access is several orders of magnitude slower than memory access—milliseconds versus nanoseconds.Accessing data from memory rather than the disk is much faster, and accessing data from the processor’s L1 or L2 cache is faster still.Second,data accessed once will, with a high likelihood, find itself accessed again in the near future.This principle—that access to a particular piece of data tends to be clustered in time—is called temporal locality, which ensures that if data is cached on its first access,there is a high probability of a cache hit (access to data in the cache) in the near future. Given that memory is so much faster than disk, coupled with the fact that once-used is likely twice-used data, an in-memory cache of the disk is a large performance win.


     同时不能将过度利用物理内存将文件全部缓存,如果过多利用物理内存缓存文件缓存到内存中将会造成其他程序无法申请过多内存。当系统物理内存比较紧张时,那么首先就会考虑到将page cahe释放掉同时将其中内容写入到磁盘中。这样既能挤出更多物理内存给程序使用,有不会造成数据丢失,大不了当再次访问文件时再次从磁盘中读取,只是此时性能较差而已。

    page cache 管理结构

    page cache管理结构如下:

    •  struct address_space:可以看作是对一个文件地址空间管理抽象,主要功能是用于管理该文件节点内的page cache,以及每个page 对应到磁盘文件中block。同时由于一个文件在磁盘中存储的block不连续,address_space可以将其抽象称一个连续空间方便后续管理。
    • 每个文件有自己专属page cache。
    • struct address_space:具有唯一性即每个文件只有一个address_space,位于inode结构中,成员为i_data(inode结构解释可以查看《linux 内核inode VS file》
    • 内核中为了方便对struct address_space使用,inode节点定义了i_mapping指针,一般用于指向本文件中的i_node->i_data即inode->i_mapping = &inode->i_data。
    • struct address_space结构中使用xarray结构用于管理缓存的page, 该结构由Matthew Wilcox开发并在 4.18合入主分支,用于替代之前使用的radix tree结构(The XArray data structure [LWN.net])。
    • struct file 为进程资源,当一个文件被多个进程打开时会每个进程会由各自专属struct file,但是每个进程的file->f_mapping指向的address_space是唯一的,只有这样当一个文件 被多个进程打开时才能实现使用一块物理内存,以便节省内存,同时容易做到内容同步。
    • struct address_space中struct address_space_operations为相应的page cache或该文件其他操作。
    • struct file中file_operations提供的时进程级别的操作。
    • page cache中缓冲的物理页面一般都是不连续的。
    • struct inode节点为文件在磁盘中表示抽象,具有唯一性, inode中超级块记录文件在磁盘中位置。
    • 文件在磁盘中是以block形式存在,x86系统一个block大小为512字节,而一般一个page大小为4K,因此一个page 可以存放8个block,且这些block大部分情况下为不连续。
    • 当物理为page cache时,说明该page存储的是文件内存,union联合体部分mmaping指向address_space,方便查找该物理页存储对应的文件以及对应中磁盘位置。

    struct address_space

    struct address_space结构为可以理解为对文件中磁盘存储空间的一种抽象,保存物理page cache与inode block中的对应关系,该结构定义位于include\linux\fs.h文件中:

    struct address_space {
    	struct inode		*host;
    	struct xarray		i_pages;
    	gfp_t			gfp_mask;
    	atomic_t		i_mmap_writable;
    #ifdef CONFIG_READ_ONLY_THP_FOR_FS
    	/* number of thp, only for non-shmem files */
    	atomic_t		nr_thps;
    #endif
    	struct rb_root_cached	i_mmap;
    	struct rw_semaphore	i_mmap_rwsem;
    	unsigned long		nrpages;
    	unsigned long		nrexceptional;
    	pgoff_t			writeback_index;
    	const struct address_space_operations *a_ops;
    	unsigned long		flags;
    	errseq_t		wb_err;
    	spinlock_t		private_lock;
    	struct list_head	private_list;
    	void			*private_data;
    } __attribute__((aligned(sizeof(long)))) __randomize_layout;
    • struct inode   *host:指向该address_space所属inode节点。
    • struct xarray   i_pages:用于记录该文件缓存的page cache。
    • gfp_t            gfp_mask:gfp mask用于记录page cache gfp标记位
    • atomic_t        i_mmap_writable:记录以共享VM_SHARED打开该文件使用者数目
    • atomic_t        nr_thps:需要配置CONFIG_READ_ONLY_THP_FOR_FS,记录该文件用于非共享页面数量。
    • struct rb_root_cached    i_mmap:用于记录所有打开该文件的进程的虚拟空间,使用红黑树管理。
    • struct rw_semaphore    i_mmap_rwsem:address_space信号量用于防止同时操作。
    • unsigned long        nrpages:缓存的物理页数目。
    • unsigned long        nrexceptional:shadow 或DAX entry.
    • pgoff_t            writeback_index:将pagecache写回磁盘在文件中的起始位置
    • const struct address_space_operations *a_ops:addree_space对应op
    • unsigned long        flags: 错误标志位和AS_*系列标志位
    • errseq_t        wb_err:最近遇到的错误
    • spinlock_t        private_lock:所挂载文件私有数据锁
    • struct list_head    private_list: 用于挂载文件metadata数据
    • void            *private_data:文件私有数据

    i_mmap

    一个存在磁盘中的文件可能同时被多个进程打开,映射到进程中的虚拟空间vma各不相同,为了能够根据address_space能够快速查找到对应进程的vma, address_space将各自进程vma对应关系存储到i_mapp结构中,这样就形成了一个文件address_space对应多进程vma 即1:N的关系。


    Here it suffices to know that the address space is the base element of a priority tree that contains all vm_area_struct instances describing the mapping of an interval of the file associated with the inode into some virtual address space. Since each instance of struct vm_area contains a pointer to the mm_struct of the process to which it belongs, the desired connection is set up! Note that vm_area_structs can also be associated with an address space via a doubly linked list headedby i_mmap_nonlinear. This is required for nonlinear mappings, which I neglect for now 

    在早期2.6版本中address_space结构对vma管理如下:

    • 2.6版本中address_space分别有两种形式进行管理相同vma分别通过prio tree树和listl链表两种形式进行管理
    • 5.8.10版本中已经演进为通过红黑树进行管理(struct rb_root_cached    i_mmap),将链表形式删除以节省内存。
    • 这张图虽然是基于2.6版本,但是整体改变不大,对5.8.10版本仍有很大借鉴意义。

    i_pages

    struct xarray   i_pages用于记录该文件中所有的page cache, 由Matthew Wilcox开发完,4.18版本之前使用radix tree结构。

    消失的backing_dev_info

    address_space在早期版本中还存在backing_dev_info结构,用于方便获取文件block:

    由于 backing_dev_info与超级块向重复,且几乎没有被使用,当获取block时可以从超级块获取,因此该结构后来被删除,以便节省内存(见[08/12] fs: remove mapping->backing_dev_info - Patchwork),上述整体结构变化:

    • 删除了 backing_dev_info结构,从磁盘中获取block可以通过超级块superblock获取。
    • 使用xarray取代了radix tree管理page cache。
    • 基本管理没有大的变化。

    struct address_space_operations

    address_space_operations 提供了对address space结构操作,主要是page cache以及page cache与磁盘之前操作包括读取,写入等:

    
    struct address_space_operations {
    	int (*writepage)(struct page *page, struct writeback_control *wbc);
    	int (*readpage)(struct file *, struct page *);
    
    	/* Write back some dirty pages from this mapping. */
    	int (*writepages)(struct address_space *, struct writeback_control *);
    
    	/* Set a page dirty.  Return true if this dirtied it */
    	int (*set_page_dirty)(struct page *page);
    
    	/*
    	 * Reads in the requested pages. Unlike ->readpage(), this is
    	 * PURELY used for read-ahead!.
    	 */
    	int (*readpages)(struct file *filp, struct address_space *mapping,
    			struct list_head *pages, unsigned nr_pages);
    	void (*readahead)(struct readahead_control *);
    
    	int (*write_begin)(struct file *, struct address_space *mapping,
    				loff_t pos, unsigned len, unsigned flags,
    				struct page **pagep, void **fsdata);
    	int (*write_end)(struct file *, struct address_space *mapping,
    				loff_t pos, unsigned len, unsigned copied,
    				struct page *page, void *fsdata);
    
    	/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
    	sector_t (*bmap)(struct address_space *, sector_t);
    	void (*invalidatepage) (struct page *, unsigned int, unsigned int);
    	int (*releasepage) (struct page *, gfp_t);
    	void (*freepage)(struct page *);
    	ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
    	/*
    	 * migrate the contents of a page to the specified target. If
    	 * migrate_mode is MIGRATE_ASYNC, it must not block.
    	 */
    	int (*migratepage) (struct address_space *,
    			struct page *, struct page *, enum migrate_mode);
    	bool (*isolate_page)(struct page *, isolate_mode_t);
    	void (*putback_page)(struct page *);
    	int (*launder_page) (struct page *);
    	int (*is_partially_uptodate) (struct page *, unsigned long,
    					unsigned long);
    	void (*is_dirty_writeback) (struct page *, bool *, bool *);
    	int (*error_remove_page)(struct address_space *, struct page *);
    
    	/* swapfile support */
    	int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
    				sector_t *span);
    	void (*swap_deactivate)(struct file *file);
    };
    

    address space作为磁盘存储与内存之前的桥梁, 提供的operation主要是用于对两者直接的数据传递功能。由于address space由各自组合,因此只能以动态函数映射方式定义以兼容各种场景:


    Address spaces connect backing stores with memory segments. Not only data structures but also functions are needed to perform the transfer operations between the two. Because address spaces can be used in various combinations, the requisite functions are not defined statically but must be determined according to the particular mapping with the help of a special structure that holds function pointers to the appropriate implementation 


    几个比较重要opertion说明: 

    •  readpage和readpages 分别是从磁盘中读取一个页或者多个页内容到page cache中,该函数实现大部分都是使用内核标准函数mpage_readpage函数实现的。同时读取问还有系统一系列函数readpages、readpages等。
    • writepage和writepages分别将page cache中一个page或者多个page内容写回磁盘中,大部分也是采用内核标准函数实现比如block_write_full_page、mpage_writepages及mpage_writepage等。
    • set_page_dirty:将页面标记位脏页,意思时此时物理内存中数据与磁盘中数据不一致,需要合适时机刷新到磁盘中。
    • 可以参考Documentation\filesystems\vfs.rst文件中有对operation详细说明。

    关于pagecache相关函数

    pagecache主要实现位于mm\filemap.c文件中

    __add_to_page_cache_locked

    __add_to_page_cache_locked函数向指定的address_space中添加指定page,,作为该页面page cache:

    
    static int __add_to_page_cache_locked(struct page *page,
    				      struct address_space *mapping,
    				      pgoff_t offset, gfp_t gfp_mask,
    				      void **shadowp)
    {
    	XA_STATE(xas, &mapping->i_pages, offset);
    	int huge = PageHuge(page);
    	int error;
    	void *old;
    
    	VM_BUG_ON_PAGE(!PageLocked(page), page);
    	VM_BUG_ON_PAGE(PageSwapBacked(page), page);
    	mapping_set_update(&xas, mapping);
    
    	get_page(page);
    	page->mapping = mapping;
    	page->index = offset;
    
    	if (!huge) {
    		error = mem_cgroup_charge(page, current->mm, gfp_mask);
    		if (error)
    			goto error;
    	}
    
    	do {
    		xas_lock_irq(&xas);
    		old = xas_load(&xas);
    		if (old && !xa_is_value(old))
    			xas_set_err(&xas, -EEXIST);
    		xas_store(&xas, page);
    		if (xas_error(&xas))
    			goto unlock;
    
    		if (xa_is_value(old)) {
    			mapping->nrexceptional--;
    			if (shadowp)
    				*shadowp = old;
    		}
    		mapping->nrpages++;
    
    		/* hugetlb pages do not participate in page cache accounting */
    		if (!huge)
    			__inc_lruvec_page_state(page, NR_FILE_PAGES);
    unlock:
    		xas_unlock_irq(&xas);
    	} while (xas_nomem(&xas, gfp_mask & GFP_RECLAIM_MASK));
    
    	if (xas_error(&xas)) {
    		error = xas_error(&xas);
    		goto error;
    	}
    
    	trace_mm_filemap_add_to_page_cache(page);
    	return 0;
    error:
    	page->mapping = NULL;
    	/* Leave page->index set: truncation relies upon it */
    	put_page(page);
    	return error;
    }
    
    • XA_STATE(xas, &mapping->i_pages, offset):初始化xarray状态机
    • PageLocked:查看page是否上锁,因为后续要对page操作刷新page 状态,故需要page已经被锁定
    • PageSwapBacked:如果该page 被swap到磁盘中则返回。
    • mapping_set_update:设置xas中address_space 地址
    • get_page:后续需要将该page加入到page cache xarray数组中 故page引用计数要加1.
    • page->mapping = mapping 设置保存物理页对应的address_space,方便后续查找。
    • page->index = offset 保存offset。
    • 如果非huge page还需要加入cgroup中。
    • 接下来就是在xarray中查找到合适的位置将page加入到xarray进行管理。
    • xas_lock_irq(&xas) 对xarray加锁。
    • xas_load 获取xarry中一个entry并对齐进行检查
    • xas_store:将物理页保存到xarray中。
    • 获取到的old entry为一个合法值,mapping->nrexceptional减一
    • mapping->nrpages++:nrpages计数+1,表明一个page作为该文件page cache。
    • xas_unlock_irq:释放锁。

    以上是将一个page 作为一个文件page cache,加入到该文件xarray中全过程。基于该函数还有两个变种函数对外部函数使用

    add_to_page_cache_lru

    add_to_page_cache_lru函数除了将page 加入到page cache中还同时将该page 加入到 inactive LRU中:

    int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
    				pgoff_t offset, gfp_t gfp_mask)
    {
    	void *shadow = NULL;
    	int ret;
    
    	__SetPageLocked(page);
    	ret = __add_to_page_cache_locked(page, mapping, offset,
    					 gfp_mask, &shadow);
    	if (unlikely(ret))
    		__ClearPageLocked(page);
    	else {
    		/*
    		 * The page might have been evicted from cache only
    		 * recently, in which case it should be activated like
    		 * any other repeatedly accessed page.
    		 * The exception is pages getting rewritten; evicting other
    		 * data from the working set, only to cache data that will
    		 * get overwritten with something else, is a waste of memory.
    		 */
    		WARN_ON_ONCE(PageActive(page));
    		if (!(gfp_mask & __GFP_WRITE) && shadow)
    			workingset_refault(page, shadow);
    		lru_cache_add(page);
    	}
    	return ret;
    }
    
    • __SetPageLocked:锁定该物理页。
    • __add_to_page_cache_locked:将该页面加入到page cache中。
    • lru_cache_add:将该页面加入到inactive LRU链表中(后续如果页面再次被使用可以通过mark_page_accessed刷新page到active LRU中)。 

    __page_cache_alloc

    __page_cache_alloc用于从buddy中申请一个物理页后续该申请的物理页作为page cache,NUMA系统中:

    struct page *__page_cache_alloc(gfp_t gfp)
    {
    	int n;
    	struct page *page;
    
    	if (cpuset_do_page_mem_spread()) {
    		unsigned int cpuset_mems_cookie;
    		do {
    			cpuset_mems_cookie = read_mems_allowed_begin();
    			n = cpuset_mem_spread_node();
    			page = __alloc_pages_node(n, gfp, 0);
    		} while (!page && read_mems_allowed_retry(cpuset_mems_cookie));
    
    		return page;
    	}
    	return alloc_pages(gfp, 0);
    }
    
    •  如果支持 spread,则从允许的cpuset中调用__alloc_pages_node申请物理内存。
    • 如果不支持,则调用alloc_pages申请物理内存

    pagecache_get_page

    • 该函数先从指定的index和address_space中是否已经有page cache,如果有则将获取到的page 返回。
    • 如果没有,且fgp_flags标志位设置为FGP_CREAT,则申请一个新的page,并加入到该文件page cache中。
    struct page *pagecache_get_page(struct address_space *mapping, pgoff_t index,
    		int fgp_flags, gfp_t gfp_mask)
    {
    	struct page *page;
    
    repeat:
    	page = find_get_entry(mapping, index);
    	if (xa_is_value(page))
    		page = NULL;
    	if (!page)
    		goto no_page;
    
    	if (fgp_flags & FGP_LOCK) {
    		if (fgp_flags & FGP_NOWAIT) {
    			if (!trylock_page(page)) {
    				put_page(page);
    				return NULL;
    			}
    		} else {
    			lock_page(page);
    		}
    
    		/* Has the page been truncated? */
    		if (unlikely(compound_head(page)->mapping != mapping)) {
    			unlock_page(page);
    			put_page(page);
    			goto repeat;
    		}
    		VM_BUG_ON_PAGE(page->index != index, page);
    	}
    
    	if (fgp_flags & FGP_ACCESSED)
    		mark_page_accessed(page);
    
    no_page:
    	if (!page && (fgp_flags & FGP_CREAT)) {
    		int err;
    		if ((fgp_flags & FGP_WRITE) && mapping_cap_account_dirty(mapping))
    			gfp_mask |= __GFP_WRITE;
    		if (fgp_flags & FGP_NOFS)
    			gfp_mask &= ~__GFP_FS;
    
    		page = __page_cache_alloc(gfp_mask);
    		if (!page)
    			return NULL;
    
    		if (WARN_ON_ONCE(!(fgp_flags & (FGP_LOCK | FGP_FOR_MMAP))))
    			fgp_flags |= FGP_LOCK;
    
    		/* Init accessed so avoid atomic mark_page_accessed later */
    		if (fgp_flags & FGP_ACCESSED)
    			__SetPageReferenced(page);
    
    		err = add_to_page_cache_lru(page, mapping, index, gfp_mask);
    		if (unlikely(err)) {
    			put_page(page);
    			page = NULL;
    			if (err == -EEXIST)
    				goto repeat;
    		}
    
    		/*
    		 * add_to_page_cache_lru locks the page, and for mmap we expect
    		 * an unlocked page.
    		 */
    		if (page && (fgp_flags & FGP_FOR_MMAP))
    			unlock_page(page);
    	}
    
    	return page;
    }
    

    replace_page_cache_page

    使用一个新的物理页将旧的page cache替换掉:

    int replace_page_cache_page(struct page *old, struct page *new, gfp_t gfp_mask)
    {
    	struct address_space *mapping = old->mapping;
    	void (*freepage)(struct page *) = mapping->a_ops->freepage;
    	pgoff_t offset = old->index;
    	XA_STATE(xas, &mapping->i_pages, offset);
    	unsigned long flags;
    
    	VM_BUG_ON_PAGE(!PageLocked(old), old);
    	VM_BUG_ON_PAGE(!PageLocked(new), new);
    	VM_BUG_ON_PAGE(new->mapping, new);
    
    	get_page(new);
    	new->mapping = mapping;
    	new->index = offset;
    
    	mem_cgroup_migrate(old, new);
    
    	xas_lock_irqsave(&xas, flags);
    	xas_store(&xas, new);
    
    	old->mapping = NULL;
    	/* hugetlb pages do not participate in page cache accounting. */
    	if (!PageHuge(old))
    		__dec_lruvec_page_state(old, NR_FILE_PAGES);
    	if (!PageHuge(new))
    		__inc_lruvec_page_state(new, NR_FILE_PAGES);
    	if (PageSwapBacked(old))
    		__dec_lruvec_page_state(old, NR_SHMEM);
    	if (PageSwapBacked(new))
    		__inc_lruvec_page_state(new, NR_SHMEM);
    	xas_unlock_irqrestore(&xas, flags);
    	if (freepage)
    		freepage(old);
    	put_page(old);
    
    	return 0;
    }
    

     delete_from_page_cache

    将一个page从page cache中删除:

    void delete_from_page_cache(struct page *page)
    {
    	struct address_space *mapping = page_mapping(page);
    	unsigned long flags;
    
    	BUG_ON(!PageLocked(page));
    	xa_lock_irqsave(&mapping->i_pages, flags);
    	__delete_from_page_cache(page, NULL);
    	xa_unlock_irqrestore(&mapping->i_pages, flags);
    
    	page_cache_free_page(mapping, page);
    }

    注意此时删除page cache中指定的page,有一个小技巧就是调用 __delete_from_page_cache函数时将shadow 置为NULL.

    __delete_from_page_cache

    __delete_from_page_cache删除指定page,在xarray中将page存在为位置置为shadow:

    void __delete_from_page_cache(struct page *page, void *shadow)
    {
    	struct address_space *mapping = page->mapping;
    
    	trace_mm_filemap_delete_from_page_cache(page);
    
    	unaccount_page_cache_page(mapping, page);
    	page_cache_delete(mapping, page, shadow);
    }

     page_cache_delete为从page cache中删除指定page 实行函数。

    page_cache_delete

    page_cache_delete从page cache中删除指定page,并将page所处位置指定为shadow,由于删除时设置为NULL,故该位置page被从xarry移除并为空:

    static void page_cache_delete(struct address_space *mapping,
    				   struct page *page, void *shadow)
    {
    	XA_STATE(xas, &mapping->i_pages, page->index);
    	unsigned int nr = 1;
    
    	mapping_set_update(&xas, mapping);
    
    	/* hugetlb pages are represented by a single entry in the xarray */
    	if (!PageHuge(page)) {
    		xas_set_order(&xas, page->index, compound_order(page));
    		nr = compound_nr(page);
    	}
    
    	VM_BUG_ON_PAGE(!PageLocked(page), page);
    	VM_BUG_ON_PAGE(PageTail(page), page);
    	VM_BUG_ON_PAGE(nr != 1 && shadow, page);
    
    	xas_store(&xas, shadow);
    	xas_init_marks(&xas);
    
    	page->mapping = NULL;
    	/* Leave page->index set: truncation lookup relies upon it */
    
    	if (shadow) {
    		mapping->nrexceptional += nr;
    		/*
    		 * Make sure the nrexceptional update is committed before
    		 * the nrpages update so that final truncate racing
    		 * with reclaim does not see both counters 0 at the
    		 * same time and miss a shadow entry.
    		 */
    		smp_wmb();
    	}
    	mapping->nrpages -= nr;
    }
    
    • XA_STATE(xas, &mapping->i_pages, page->index):指定page 在xarray中所处位置
    • xas_store(&xas, shadow):将shadow替代 page所处的xarray位置,将page被从page cache中移除。
    • xas_init_marks: 将page所处xarray位置初始化。
    • page->mapping = NULL:page中的mapping置为NULL。
    • 如果shadow不为空,则nrexceptional计数加+1 ,并食堂smo_wmb在多核中使用barrier都同步。
    • mapping->nrpages -= nr: nrpages减nr,如果是compound page不止要减去一个page。

    参考资料

    The XArray data structure [LWN.net]

    XArray and the mainline [LWN.net]

    [08/12] fs: remove mapping->backing_dev_info - Patchwork

    《professional linux kernel architecture》

    展开全文
  • MySQL · 性能优化 · PageCache优化管理

    千次阅读 2020-10-12 10:37:07
    研究后发现是日志文件的page cache占用了大量的内存(200G+),导致系统可立即分配的内存不足,影响了系统性能。 查看linux内核文档发现,操作系统在内存的使用未超过上限时,不会主动释放page cache,以求达到最高...

    背景

    监控线上实例时,曾出现可用内存不足,性能发生抖动的情况。研究后发现是日志文件的page cache占用了大量的内存(200G+),导致系统可立即分配的内存不足,影响了系统性能。

    查看linux内核文档发现,操作系统在内存的使用未超过上限时,不会主动释放page cache,以求达到最高的文件访问效率;当遇到较大的内存需求,操作系统会当场淘汰一些page cache以满足需求。由于page cache的释放较为费时,新的进程不能及时得到内存资源,发生了阻塞。

    据此,考虑能否设计一个优化,在page cache占据大量内存前,使用linux内核中提供的posix_fadvise等缓存管理方法,由Mysql主动释放掉无用的page cache,来缓解内存压力。本文先介绍文件的page cache机制,并介绍应用程序级的管理方法,最后介绍针对Mysql日志文件的内存优化。

    Page Cache机制

    页面缓存(Page Cache)是Linux内核中针对文件I/O的一项优化,Linux从内存中划出了一块区域来缓存文件页,如果要访问外部磁盘上的文件页,首先将这些页面拷贝到内存中,再进行读写。由于硬件结构限制,磁盘的I/O速度比内存慢很多,因此使用Page cache能够大大加速文件的读写速度。 

    Page Cache的机制如上图所示,具体来说,当应用程序读文件时,系统先检查读取的文件页是否在缓存中;如果在,直接读出即可;如果不在,就将其从磁盘中读入缓存,再读出。此时如果内存有足够的内存空间,该页可以在page cache中驻留,其他进程再访问该部分数据时,就不需要访问磁盘了。

    同样,在写文件之前,系统先检查对应的页是否已经在缓存中;如果在,就直接将数据写入page cache,使其成为脏页(drity page)等待刷盘;如果不在,就在缓存中新增一个页面并写入数据(这一页面也是脏页)。真正的磁盘I/O会由操作系统调用fsync等方法来实现,这一调用可以是异步的,保证磁盘I/O不影响文件读写的效率。 在Mysql中,我们说的写文件(write)通常是指将数据写入page cache中,而刷盘或落盘(fsync)才真正将数据写入磁盘中的文件。

    程序将数据写入page cache后,可以主动进行刷脏(如调用fsync),也可以放手不管,等待内核帮忙刷脏。在linux内核中,有关自动刷脏的参数如下。

    dirty_background_ratio
    // 触发文件系统异步刷脏的脏页占总可用内存的最高百分比,当脏页占总可用内存的比例超过该值,后台回写进程被触发进行异步刷脏。
    
    dirty_ratio
    // 触发文件系统同步刷脏的脏页占总可用内存的最高百分比,当脏页占总可用内存的比例超过该值,生成新的写文件操作的进程会先执行刷脏。
    
    dirty_background_bytes & dirty_bytes
    // 上述两种刷脏条件还可通过设置最高字节数而非比例触发。如果设置bytes版本,则ratio版本将变为0,反之亦然。
    
    dirty_expire_centisecs
    // 这个参数指定了脏页多长时间后会被周期性刷脏。下次周期性刷脏时,脏页存活时间超过该值的页面都将被刷入磁盘。
    
    dirty_writeback_centisecs 
    // 这个参数指定了多长时间唤醒一次刷脏进程,检查缓存并刷下所有可以刷脏的页面。该参数设为零内核会暂停周期性刷脏。
    

    Page Cache默认由系统调度分配,当free的内存高于内核的低水位线(watermark[WMARK_MIN])时,系统会尽量让用户充分使用缓存,因为它认为这样内存的利用效率最高;当低于低水位线时,就按照LRU的顺序回收page cache。正是这种策略,使得内存的free的部分越来越小,cache的部分越来越大,造成了文章开头提到的问题。

    实际上,Mysql中许多文件有着固定的访问模式,它们的页面不会被短时间内多次访问,例如redo log和binlog文件。在实例正常运行的状态下,Redo log只是持久化每次操作的物理日志,写入文件后就没有读操作;binlog文件在写入后,也只会被dump线程所访问。

    Page Cache监控与管理

    vmtouch

    vmtouch工具可以用来查看指定文件page cache使用情况,也可以手动将文件换入或换出缓存。下面是其常用功能的使用方法

    # 显示文件的page cache使用情况
    $ vmtouch -v [filename]
    
    # 换出文件的page cache
    # 即使换出成功,内核也可能在vmtouch命令完成时将页面分页回内存。
    $ vmtouch -ve [filename]
    
    # 换入文件的page cache
    # 保证文件的page cache都换入内存,但是在vmtouch命令完成时,该页面可能被内核逐出
    $ vmtouch -vt [filename]
    

    posix_fadvise

    posix_fadvise是linux上控制页面缓存的系统函数,应用程序可以使用它来告知操作系统,将以何种模式访问文件数据,从而允许内核执行适当的优化。其中一些建议可以只针对文件的指定范围,文件的其他部分不生效。 这一函数对内核提交的是建议,在特殊情况下也可能不会被内核所采纳。

    函数在内核的mm/fadvise.c中实现,函数的声明如下:

    SYSCALL_DEFINE(fadvise64_64)(int fd, loff_t offset, loff_t len, int advice)
    

    其中fd是函数句柄;offset是建议开始生效的起始字节到文件头的偏移量;len是建议生效的字节长度,值为0时代表直到文件末尾;advice是应用程序对文件页面缓存管理的建议,共有六种合法建议。下面根据代码,对六种建议进行分析。

    switch (advice) {    
        /*
        该文件未来的读写模式位置,应用程序没有关于page cache管理的特别建议,这是advice参数的默认值
        将文件的预读窗口大小设为下层设备的默认值
        */
        case POSIX_FADV_NORMAL:
            file->f_ra.ra_pages = bdi->ra_pages;
            spin_lock(&file->f_lock);
            file->f_mode &= ~FMODE_RANDOM;
            spin_unlock(&file->f_lock);
            break;
        
        /* 该文件将要进行随机读写,禁止预读 */
        case POSIX_FADV_RANDOM:
            spin_lock(&file->f_lock);
            file->f_mode |= FMODE_RANDOM;
            spin_unlock(&file->f_lock);
            break;
        
        /*
        该文件将要进行顺序读写操作(从文件头顺序读向文件尾)
        将文件的预读窗口大小设为默认值的两倍
        */
        case POSIX_FADV_SEQUENTIAL:
            file->f_ra.ra_pages = bdi->ra_pages * 2;
            spin_lock(&file->f_lock);
            file->f_mode &= ~FMODE_RANDOM;
            spin_unlock(&file->f_lock);
            break;
        
        /* 该文件只会被访问一次,收到此建议时,什么也不做 */    
        case POSIX_FADV_NOREUSE:
            break;
        
        /* 该文件将在近期被访问,将其换入缓存中 */
        case POSIX_FADV_WILLNEED:
            ...
            ret = force_page_cache_readahead(mapping, file,
                                             start_index,
                                             nrpages);
            ...
            break;
        
        /* 该文件在近期内不会被访问,将其换出缓存 */
        case POSIX_FADV_DONTNEED:
            if (!bdi_write_congested(mapping->backing_dev_info))
                __filemap_fdatawrite_range(mapping, offset, endbyte,
                                           WB_SYNC_NONE);
            ...
            if (end_index >= start_index)
                invalidate_mapping_pages(mapping, start_index,
                                         end_index);
            break;
        default:
            ret = -EINVAL;
    }
    
    

    针对POSIX_FADV_NORMAL,POSIX_FADV_RANDOM和POSIX_FADV_SEQUENTIAL这三个建议,内核会对文件的预读窗口大小做调整,具体调整策略见代码注释。这些建议的影响范围是整个文件(无视offset和len参数),但不影响该文件的其他句柄。针对POSIX_FADV_WILLNEED和POSIX_FADV_DONTNEED,内核会尝试直接对page cache做调整,这里不是强制的换入或换出,内核会根据情况采纳建议。

    当建议为POSIX_FADV_WILLNEED时,内核调非阻塞读force_page_cache_readahead方法,将数据页换入缓存。这里根据内存负载的情况,内核可能会减少读取的数据量。

    当建议为POSIX_FADV_DONTNEED时,内核先调用fdatawrite将脏页刷盘。这里刷脏页用的参数是非同步的WB_SYNC_NONE。刷完脏后,会调用invalidate_mapping_pages清除相关页面,该函数在mm/truncate.c中实现,代码如下。

    unsigned long invalidate_mapping_pages(struct address_space *mapping,
            pgoff_t start, pgoff_t end)
    {
        ...
        while (index <= end && pagevec_lookup(&pvec, mapping, index,
                min(end - index, (pgoff_t)PAGEVEC_SIZE - 1) + 1)) {
            mem_cgroup_uncharge_start();
            for (i = 0; i < pagevec_count(&pvec); i++) {
                ...
                ret = invalidate_inode_page(page);
                ...
            }
            ...
        }
        return count;
    }
    
    int invalidate_inode_page(struct page *page)
    {
        struct address_space *mapping = page_mapping(page);
        if (!mapping)
            return 0;
        if (PageDirty(page) || PageWriteback(page))
            return 0;
        if (page_mapped(page))
            return 0;
        return invalidate_complete_page(mapping, page);
    }
    

    可以看到,invalidate_mapping_pages调用了下层函数invalidate_inode_page,其中的判断逻辑是,如果页脏或页正在写回,则什么也不做;如果没有映射关系或页不再缓存中,则什么也不做。所以这里内核只会尽力而为,清除掉自己能清除的缓存,而不会等待刷脏完成后再清除文件的全部缓存。

    因此,在使用POSIX_FADV_DONTNEED参数清除page cahce时,应当先执行fsync将数据落盘,这样才能确保page cache全部释放成功。posix_fadvise函数包含于头文件fcntl.h中,清除一个文件的page cache的方法如下:

    #include <fcntl.h>
    #include <unistd.h>
    ...
    fsync(fd);
    int error = posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
    ...
    

    posix_fadvise成功时会返回0,失败时可能返回的error共有三种,分别是

    EBADF  // 参数fd(文件句柄)不合法,值为9
    
    EINVAL // 参数advise不是六种合法建议之一,值为22
    
    ESPIPE // 输入的文件描述符指向了管道或FIFO,值为29
    

    Mysql日志优化策略

    Mysql中不同文件有着不同的访问行为,日志文件是一种顺序读写占绝大多数的文件,因此我们可以为binlog和redo log设计相应的管理策略,来清除暂时不会使用的page cache。

    Page cache是系统资源,不属于某个进程管理,因此无法通过进程内存使用的情况来观察优化效果。我们可以使用vmtouch工具来查看page cache的使用情况。

    Redo log

    在Innodb层,Redo log的主要职责是数据持久化,实现先写日志再写数据的WAL机制。Redo log文件大小和个数固定,由innodb_log_file_size和innodb_log_files_in_group参数控制,这些文件连在一起,被Innodb当成一个整体循环使用。

    Redo log写page cache和刷盘分别由线程log_writer和log_flusher异步执行,8.0版本中还实现了写log buffer的无锁化,其具体可参见往期月报:http://mysql.taobao.org/monthly/2019/02/05/

    在正常运行的实例中,redo log只有写操作。在写入某个文件的某页后,需要较长的一段时间log_flusher才会再次推进到该页,因此无需保留page cache。

    Redo Log的刷盘由log_flusher线程异步执行,因此可以将page cache的释放操作放在log_flusher线程上,每次flush刷脏后执行。这样每次需要释放的page cache较少,耗时较短;

    Binlog

    在mysql实例正常运行过程中,binlog主要用来做主从复制。Binlog文件的大小由参数max_binlog_size指定,数量没有限制。Binlog的刷盘由参数sync_binlog控制,当sync_binlog为0的时候,刷盘由操作系统负责,异步执行;当不为0的时候,其数值为定期sync磁盘的binlog commit group数,由主线程同步执行刷盘。

    没有从库挂载时,binlog只有写操作,保留page cache意义不大。sync_binlog大于0时,刷盘操作以事务为单位,在主线程中拿LOCK_log锁同步执行,如果在每次刷盘后进行fadvise,会阻塞较多的主线程。

    因此,将page cache的释放延后到rotate执行,即在关闭旧文件并且成功开启新文件,放掉LOCK_log锁后,释放旧文件的page cache。这样,fadvise操作只会阻塞负责rotate的线程,不会影响到其他线程(因为其他线程都在新的binlog文件中操作)。Rotate执行过程中会调用sync方法刷脏,因此在rotate后释放page cache无需提前刷脏。

    有从库挂载时,每次binlog刷盘后,会有dump线程来读取binlog文件的更新,并将更新内容发送到从库。当binlog文件的最后写入位置与dump线程的读取位置比较近(如相距3个文件以内)时,在dump线程读完binlog后再释放page cache效率较高,因为dump可以从page cache中读到更新内容,无需磁盘I/O。这种情况下,将page cache的释放延后到dump线程rotate成功后执行。Dump线程切换binlog时,旧文件已被主线程刷脏,而dump线程只会做读操作,因此不会产生脏页,释放page cache前无需再次刷脏。

    测试

    测试机规格:96core,750GB memory 设置binlog的大小为1GB,redo log的大小为1GB,数量为10个;设置innodb_flush_log_at_trx_commit和sync_binlog为双1。使用sysbench工具进行压测,线程数为128,模式为OLTP_read_write,结果如下图所示。   上面两个图分别是有从库挂载和没有从库挂载时的性能测试结果,蓝色曲线是不加page cache优化时的QPS折线图,橙色曲线是加page cache优化时的QPS折线图。可以发现,释放page cache的优化基本不会导致性能的下降。使用write only模式sysbench压测也无明显性能损失,这里不再展开叙述。 

    使用vmtouch工具测试page cache释放的速度,如上图所示。在测试机上清理实例写满的1GB的redo log file的page cache约耗时0.28秒。

     

    上面两个图是无从库挂载,无page cache优化时,压测300s后data文件夹的cache使用情况和系统的内存使用情况统计。使用vmtouch命令查看实例的data文件夹,可以看到不使用优化时,cache激增到20G。使用free命令查看系统内存的使用情况,发现大部分的内存被cache占用,比free的部分多6.8倍。

     

    上面两个图是有page cache优化时,压测300s后内存的使用情况。可以看到,使用优化后,实例的data文件夹使用cache的大小显著减小。使用free命令,发现减少的cache全部计入了free中,可以被自由使用。

    挂载从库进行同样的测试,可得到相同的结论。

    总结

    系统对page cache的管理,在一些情况下可能有所欠缺,我们可以通过内核提供的posix_fadvise予以干预。在几乎不损失性能的前提下,可以通过主动释放Mysql日志文件的page cache的方法,达到减缓内存压力的目的。对于极端内存需求的场景,这一优化能够很好的预防性能抖动的发生。

    展开全文
  • 页缓存(PageCache)是在中心缓存(CentralCache)下面的一层缓存,存储的内存是以页为单位存储及分配的,中心缓存没有内存对象时,从PageCache分配出一定数量的页(一页是8KB),把它们切割成定长大小的小块内存,...

    一. PageCache介绍

    页缓存(PageCache)是在中心缓存(CentralCache)下面的一层缓存机制,它存储的是以页为单位的未切分大块跨度内存。中心缓存没有Span对象时,从PageCache分配出一定数量页(一页是8KB)的Span,把它们切割成定长大小的小块内存,分配给中心缓存。当中心缓存中一个Span的所有小块定长内存都回收以后,PageCache会回收中心缓存的Span对象,拿回来合并相邻页,组成更大的页,以缓解内存碎片的问题。
    在这里插入图片描述

    二. PageCache基本框架

    下面是PageCache的基本框架,其核心成员是SpanList对象组成的哈希桶:

    // 饿汉的单例模式
    class PageCache
    {
    public:
    	// 返回PageCache的单例对象
    	static PageCache* GetInstance()
    	{
    		return &_sInst;
    	}
    
    	// 返回给CentralCache一个k页的Span
    	Span* NewSpan(size_t k);
    
    	// 获取表锁
    	std::mutex& GetPageMutex();
    
    private:
    	std::mutex _pageMtx;// 表锁
    	SpanList _spanLists[NPAGES];// 存储未切分的span
    
    private:
    	PageCache()
    	{}
    
    	PageCache(const PageCache&) = delete;
    
    	static PageCache _sInst;
    };
    

    虽然CentralCache和PageCache的核心结构都是SpanList对象组成的哈希桶,但是它们是有区别的:

    • CentralCache中的哈希桶,跟ThreadCache一样是按照对象大小对齐后映射小块定长内存大小的,它的每一个SpanList中挂的Span都按映射的值切成一个个小块定长内存存储在自由链表中。
    • PageCache中的SpanList是按照Span存储的页的数量直接定址映射的,也就是说第i号桶中挂的Span都存储有i页的连续内存空间,且这些连续页空间是未被切分成小块定长内存的。
      在这里插入图片描述

    我们规定PageCache中的Span对象最多存储128页的连续内存。因为下标从0开始,所以我们创建129个桶,这样桶的下标和页数就是完全一对一的直接映射:

    // 把这个写到Common.h中
    static const size_t NPAGES = 129;// PageCache可申请的最大页数
    

    另外和CentralCache不同的是,PageCache不再使用SpanList中定义的桶锁,而是在自己类中封装一个针对其成员PageCache::_spanLists[NPAGES]整个哈希表的表锁,这样设计的原因从两个不同的角度来看:

    1. 首先CentralCache为什么使用桶锁呢?因为每一次不论是CentralCache向上供给ThreadCache一批小块定长内存,还是向下(PageCache)申请一个特定页大小的Span对象都只需访问CentralCache特定的一个桶就够了,这个时候如果用表锁,反而会降低效率。
    2. 然后是PageCache,现在它的功能是拨给CentralCache一个特定数量页的Span对象,比如CentralCache向PageCache申请4页的Span对象,PageCache发现自己的4号桶后面没有挂Span,则向后面的桶中去寻找更大页的Span,假设在10号桶位置找到一个Span,则将链表中的第一个10页的Span分裂为一个4页Span和一个6页Span,4页的Span对象返回给CentralCache,另外一个6页的Span挂到PageCache自己的6号桶中。即我们对PageCache进行一次操作不是仅仅使用它的一个桶,这个时候如果使用桶锁就需要频繁地申请、释放锁,这样做固然不会有线程安全的问题,但是对性能有消耗,不如干脆定义一个表锁更好。

    初步设计的PageCache只有三个成员函数:

    • PageCache::GetInstance():获取进程中唯一的PageCache的单例对象。
    • PageCache::GetPageMutex():获取PageCache的表锁。
    • PageCache::NewSpan(size_t k):返回给CentralCache一个k页的Span对象。

    三. 其它接口和数据的补充

    1. 直接向堆申请页为单位的大块内存

    各个系统通过系统调用直接向堆申请内存空间的方法:

    • Windows:VirtualAlloc是一个Windows API函数,该函数的功能是在调用进程的虚地址空间,预定或者提交一部分页。
    • Linuxs:使用brk、mmap

    针对所有平台,我们统一封装一个SystemAlloc()函数放到Common.h中,该函数只需传入需要向堆申请的页的数量即可,内部完成后会返回申请到的大块连续页内存的起始地址:

    // PS:目前下面只写了Windows平台的
    // Linux平台的后面在完善
    
    // 条件编译不同系统的系统头文件
    #ifdef _WIN32
    	#include <windows.h>
    #else
    	// Linux
    #endif
    
    // 条件编译不同系统到堆上以页为单位申请空间
    inline static void* SystemAlloc(size_t kpage)
    {
    #ifdef _WIN32
    	void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    #else
    	// linux下brk mmap等
    #endif
    	if (ptr == nullptr)
    		throw std::bad_alloc();
    
    	return ptr;
    }
    

    2. SpanList类中的补充

    在上一篇文章中有说明SpanList类,它是管理Span的双向带头循环链表,下面是它的基本框架:

    // 管理span的带头双向循环链表
    class SpanList
    {
    public:
    	// 构造函数中new一个哨兵位的头结点
    	SpanList()
    	{
    		_head = new Span;
    		_head->_prev = _head;
    		_head->_next = _head;
    	}
    
    	// 在pos位置插入一个新的大块跨度内存
    	void Insert(Span* pos, Span* newSpan);
    
    	// 移除pos位置的一个大块跨度内存
    	void Erase(Span* pos);
    	
    	// 获取桶锁
    	std::mutex& GetSpanListMutex();
    
    private:
    	Span* _head;	 // 哨兵位的头结点
    	std::mutex _mtx; // 桶锁
    };
    

    这次我们要在SpanList类中补充5个成员函数,下面在类里的成员函数都是新补充的:

    // 管理span的带头双向循环链表
    class SpanList
    {
    public:
    	// 获取第一个Span
    	Span* Begin()
    	{
    		return _head->_next;
    	}
    
    	// 获取哨兵位头结点
    	Span* End()
    	{
    		return _head;
    	}
    
    	// 判空
    	bool Empty()
    	{
    		return Begin() == _head;
    	}
    
    	// 头删一个span对象并返回给外部
    	Span* PopFront()
    	{
    		assert(!Empty());
    
    		Span* ret = Begin();
    		Erase(Begin());
    		return ret;
    	}
    
    	// 头插一个span对象
    	void PushFront(Span* span)
    	{
    		assert(span);
    
    		Insert(Begin(), span);
    	}
    	
    private:
    	Span* _head;	 // 哨兵位的头结点
    	std::mutex _mtx; // 桶锁
    };
    

    3. SizeClass类中的补充

    我们来梳理申请内存的顺序:

    • 首先是线程向自己的ThreadCache申请一个小于256KB的对象空间,ThreadCache发现映射后的自由链表桶中没有对齐后的小块定长内存了,这时它向下一层CentralCache通过慢启动反馈调节法去批量要小块定长内存。
    • CentralCache发现自己对应桶中的Span没有切分好的小块定长内存,或者桶里直接就没有Span对象,这时CentralCache继续向下一层PageCache去要一个特定数量页的Span过来,然后CentralCache把这个Span切分成一个个小块定长内存,把它们一部分交给ThreadCache,一部分挂到自己桶里。

    CentralCache向下一层PageCache去要一个特定数量页的Span过来,这个特定数量指的是多少呢?至少我们得保证这个连续页的大内存切分成小块定长内存后能满足CentralCache向上批发给ThreadCache的数量,最好还能剩余部分小块定长内存给CentralCache,基于这个思想我们综合设计了下面的方法:

    // 计算一次向系统获取几个页合适
    // 单个对象 8byte --- 分给CentralCache一个1页的Span
    // ...
    // 单个对象 256KB --- 分给CentralCache一个64页的Span
    static size_t SizeClass::NumMovePage(size_t alignBytes)
    {
    	//  LimitSize函数的定义在下面
    	size_t num = LimitSize(alignBytes);
    	size_t npage = num * alignBytes;
    
    	npage >>= PAGE_SHIFT;
    	if (npage == 0)
    		npage = 1;
    
    	return npage;
    }
    
    // PS:这个LimitSize函数在上一篇有讲过
    // ThreadCache一次从中心缓存批量获取小块定长内存数量的上、下限
    static size_t LimitSize(size_t alignBytes)
    {
    	assert(alignBytes != 0);
    
    	// [2, 512],一次批量移动多少个对象的(慢启动)上限值
    	// 大对象(最大256KB)一次批量下限为2个
    	// 小对象(最小8字节)一次批量上限为512个
    	size_t num = MAX_BYTES / alignBytes;
    	if (num < 2)
    		num = 2;
    
    	if (num > 512)
    		num = 512;
    
    	return num;
    }
    

    补充说明
    PAGE_SHIFT的值是无符号整型数字13

    static const size_t PAGE_SHIFT = 13;
    

    在这里插入图片描述

    四. PageCache成员函数的实现

    在这里要着重说明PageCache::NewSpan(size_t k)这个函数,它的作用是返回一个k页的Span对象给上一层的CentralCache。

    首先我们通过传入的参数k直接定址找到PageCache对应存储k页Span的SpanList桶PageCache::_spanLists[k],看看桶中是否挂有Span,有的话头删这个桶中的Span并返回给外部:

    // 1、根据k直接定址映射,看对应SpanList桶中是否挂有Span
    if (!_spanLists[k].Empty())
    {
    	return _spanLists[k].PopFront();
    }
    

    如果k页的SpanList桶中没有挂Span,那么继续往下找更大页的Span,找到之后切分这个更大页的Span,切出k页Span给外部,剩下n-k页的Span挂到下标为n-k的SpanList桶中:

    // 2、走到这里说明定址映射的桶中没有Span,那么看更大页的桶是否有Span
    for (size_t n = k + 1; n < NPAGES; ++n)
    {
    	// 更大页的桶有Span的话就对其进行切分
    	if (!_spanLists[n].Empty())
    	{
    		Span* kSpan = new Span();
    		Span* nSpan = _spanLists[n].PopFront();
    
    		kSpan->_n = k;
    		kSpan->_pageId = nSpan->_pageId;
    
    		nSpan->_n -= k;
    		nSpan->_pageId += k;
    		_spanLists[nSpan->_n].PushFront(nSpan);
    
    		return kSpan;
    	}
    }
    

    PS:我们可以通过起始页的编号来计算起始页的地址,反过来也能计算:
    在这里插入图片描述

    如果更大页的Span都没有,那么就向堆中申请一个128页的连续大块未切分内存,然后从中切出需要的k也Span:

    // 3、走到这一步说明整个PageCache中找不到一个>=k页的Span,这时向堆申请一个128页的Span
    Span* bigSpan = new Span;
    
    void* ptr = SystemAlloc(NPAGES - 1);
    
    bigSpan->_n = NPAGES - 1;
    bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
    
    _spanLists[NPAGES - 1].PushFront(bigSpan);
    
    return NewSpan(k);
    

    PS:申请到128页大块内存后为了代码简洁就递归再次调用NewSpan(...)函数去执行第二步切分更大Span的逻辑,既然我们都知道要重新切了,再这样递归调用是否会消耗性能呢?答案是不会的,递归后从头开始执行到第二步的切分逻辑最多也不过128次循环而已,计算机处理128次循环须臾之间就能完成,所以为了代码简洁,我们直接递归调用就行。

    函数完整代码实现:

    // 返回给CentralCache一个k页的Span
    Span* PageCache::NewSpan(size_t k)
    {
    	assert(k > 0 && k < NPAGES);
    	// 1、根据k直接定址映射,看对应SpanList桶中是否挂有Span
    	if (!_spanLists[k].Empty())
    	{
    		return _spanLists[k].PopFront();
    	}
    	// 2、走到这里说明定址映射的桶中没有Span,那么看更大页的桶是否有Span
    	for (size_t n = k + 1; n < NPAGES; ++n)
    	{
    		// 更大页的桶有Span的话就对其进行切分
    		if (!_spanLists[n].Empty())
    		{
    			Span* kSpan = new Span();
    			Span* nSpan = _spanLists[n].PopFront();
    
    			kSpan->_n = k;
    			kSpan->_pageId = nSpan->_pageId;
    
    			nSpan->_n -= k;
    			nSpan->_pageId += k;
    			_spanLists[nSpan->_n].PushFront(nSpan);
    
    			return kSpan;
    		}
    	}
    	// 3、走到这一步说明整个PageCache中找不到一个>=k页的Span,这时向堆申请一个128页的Span
    	Span* bigSpan = new Span;
    
    	void* ptr = SystemAlloc(NPAGES - 1);
    
    	bigSpan->_n = NPAGES - 1;
    	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
    
    	_spanLists[NPAGES - 1].PushFront(bigSpan);
    
    	return NewSpan(k);
    }
    

    五. CentralCache申请一个非空的span

    CentralCache中有一个函数CentralCache::GetOneSpan(...)用于申请一个非空的span,我们现在可以实现这个函数了:

    // 从特定的SpanList中获取一个非空的Span
    Span* CentralCache::GetOneSpan(SpanList& list, size_t alignBytes)
    {
    	assert(alignBytes >= 8);
    	// 1、先到链表中找非空的Span
    	Span* it = list.Begin();
    	while (it != list.End())
    	{
    		if (it->_freeList != nullptr)
    		{
    			return it;
    		}
    		else
    		{
    			it = it->_next;
    		}
    	}
    	// 2、走到这里说明没有list中没有非空的Span,那么就需要到PageCache申请特定页大小的Span
    	// 这里可以释放桶锁,后面逻辑不再访问该桶
    	list.GetSpanListMutex().unlock();
    	// 申请PageCache的表锁,到PageCache中申请特定页的Span过来
    	PageCache::GetInstance()->GetPageMutex().lock();
    	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(alignBytes));
    	PageCache::GetInstance()->GetPageMutex().unlock();
    	// 把从PageCache申请到的Span切成一个个的小块定长内存
    	char* start = (char*)(span->_pageId << PAGE_SHIFT);// 起始页地址
    	size_t bytes = span->_n << PAGE_SHIFT;// 连续页的字节数
    	char* end = start + bytes;// 最后一页的最后地址
    
    	span->_freeList = start;
    	start += alignBytes;
    	void* tail = span->_freeList;
    	while (start < end)
    	{
    		NextObj(tail) = start;
    		tail = start;
    		start += alignBytes;
    	}
    	NextObj(tail) = nullptr;
    	// 3、重新申请桶锁,把切好的Span挂到桶上
    	list.GetSpanListMutex().lock();
    	list.PushFront(span);
    
    	return span;
    }
    
    展开全文
  • 【图解Linux内核】Page Cache

    千次阅读 2021-06-20 19:12:21
    在资深开发的日常,经常能遇见和Page Cache相关场景: 服务器的load飙高 服务器的I/O吞吐飙高 业务响应时延出现大的毛刺 业务平均访问时延明显增加。 这些问题,很可能是由于Page Cache管理不到位引起的,因为Page...
  • PageCache:第5个版本

    2021-05-23 14:26:10
    PageCache 第五个版本 模拟静态页面、页面静态化、提升动态页面性能 目前已经稳定运行在生产环境中。 PageCache 简要说明 顾名思义 PageCache 是一种缓存技术 ,是相对传统生成静态HTML的替代方式,更节约时间,只用...
  • Page Cache引起的业务问题处理

    千次阅读 2021-03-03 16:06:24
    在工作中,你可能遇见过与 Page Cache 有关的场景,比如: 服务器的 load 飙高; 服务器的 I/O 吞吐飙高; 业务响应时延出现大的毛刺; 业务平均访问时延明显增加。 这些问题,很可能是由于 Page Cache 管理不到位...
  • linux的page cache策略

    千次阅读 2021-05-13 10:43:27
    1.buffer与cached执行free命令出来的结果,包括buff和cache,实际可用的内存需要看第二行。free命令产生的buffers和cached的区别参考以下文章:2.控制cacheLinux对cache的使用没有规定上限,因为,物理内存不是拿来...
  • SimplyAdmire.Neos.PageCache

    2021-06-10 15:05:42
    SimplyAdmire.Neos.PageCache nginx 重写规则示例 error_page 418 = @uncached; location / { if ($http_cookie = 'TYPO3_Flow_Session') { return 418; } if ($request_method !~ ^(GET|HEAD)$ ) { return...
  • 所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。...
  • Linux读写缓存Page Cache

    2022-02-23 09:48:12
    一、 读写缓存Page Cache Linux对文件读写并不是每次都进行磁盘IO,而是将对应的磁盘文件缓存到内存上,之后对该文件的操作实际上也是对内存的读写。 缓存俗称页缓存(page cache),而被修改过但还没写入磁盘的页缓存...
  • Page Cache(页缓存)

    2021-03-01 07:21:27
    address_spacestruct address_space {struct inode *host; /* owning inode */struct radix_tree_root page_tree; /* radix tree of all pages */spinlock_t tree_l...
  • Kafka对PageCache的使用

    千次阅读 2020-07-27 20:16:12
    page cache & buffer cache 执行free命令,注意到会有两列名为buffers和cached,也有一行名为“-/+ buffers/cache”。 ~ free -m total used free shared buffers cached Mem: 128956 96440 32515 0 5368 ...
  • Magento-Guidance_PageCache

    2021-07-11 07:42:54
    安装将src/的内容复制到您的 Magento 安装中清除 Magento 缓存使用 Modman 安装 cd /path/to/magento/modman initmodman clone https://github.com/guidancesrobb/Magento-Guidance_PageCache.git执照在 Apache 许可...
  • page cache简介

    千次阅读 2019-12-31 17:42:44
    文件系统层级的缓存:page cache用于缓存文件的页数据,从磁盘中读取到的内容是存储在page cache里的。 结构 在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另一个是 Buf...
  • linux pagecache限制与查看

    千次阅读 2020-06-19 17:59:38
    在linux服务器使用过程中,由于linux对内存的使用原则是能cache就尽量cache,所以会出现pagecache占用很多的情况。 suse的版本有一个pagecachelimit的功能,centos中没有看到。即便是将这个功能合入到centos中,也...
  • 什么是页缓存(Page Cache)(转载)

    千次阅读 2021-08-17 13:46:30
    由于读写硬盘的速度比读写内存要慢很多(DDR4 内存读写速度是机械硬盘500倍,是固态硬盘的200倍),所以为了避免每次读写文件时,都需要对硬盘进行读写操作,Linux 内核使用页缓存(Page Cache)机制来对文件中的...
  • Kafka高性能核心——pageCache与zeroCopy

    千次阅读 2021-12-15 22:30:10
    PageCache与ZeroCopy PageCache即页面缓存,它是操作系统实现的一种主要的磁盘缓存,它的目的就是减少对磁盘IO的操作。 具体来说,就是把磁盘中的数据缓存到内存里,然后把对磁盘的访问变成对内存的访问。 目前很多...
  • 理解page cache的设计与作用

    千次阅读 2020-10-26 17:07:39
    pagecache刷写磁盘参数配置
  • Linux中的Page cache和Buffer cache详解

    万次阅读 2019-04-19 14:55:49
    这是因为当你读写文件的时候,Linux内核为了提高读写的性能和速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory 也不会自动释放,...
  • 页高速缓存(Page Cache

    千次阅读 2022-03-22 16:00:32
    页高速缓存 Page Cache Linux
  • 页面缓存(Page Cache

    千次阅读 2022-03-08 21:35:39
    这里写目录标题什么是页缓存页缓存的实现 什么是页缓存 我们知道文件一般存放在硬盘(机械硬盘或固态硬盘)中,CPU 并不能直接访问硬盘中的数据,而是需要先将硬盘中的数据读入到内存中,然后才能被 CPU 访问。...
  • 总览 ...1 标准 I/O 是写的 (write) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存);如果是读的 (read) 话则 是先从内核缓冲区拷
  • page cache中有一部分磁盘文件的缓存,因为从磁盘中读取文件比较慢, 所以读取文件先去page cache中去查找,如果命中,则不需要去磁盘中读取,大大加快读取速度。在 Linux 内核中,文件的每个数据块最多只能对应一...
  • 我们可以认为 filechannel.write 写入 PageCache 便是完成了落盘操作,但实际上,操作系统最终帮我们完成了 PageCache 到磁盘的最终写入,理解了这个概念,你就应该能够理解 FileChannel 为什么提供了一个 force() ...
  • Buffer CachePage Cache

    千次阅读 2018-05-25 00:01:06
    概念如高速缓存(cache)产生的原理类似,在I/O过程中,读取磁盘的速度相对内存读取速度要慢的多。因此为了能够加快处理数据的速度,需要将读取过的数据缓存在内存里。而这些缓存在内存里的数据就是高速缓冲区...
  • A Parallel Page Cache: IOPS and Caching for Multicore SystemsDa Zheng, Randal Burns Department of Computer ScienceJohns Hopkins UniversityAlexander S. Szalay Department of Physics and AstronomyJohns ...
  • Netty - 探究PageCache磁盘高速缓存

    千次阅读 2022-05-05 22:32:59
    文章目录PageCache磁盘高速缓存在线地址 PageCache磁盘高速缓存 在线地址 图都在processon上画好了, 点击这里
  • magento 加速插件 full page cache 效果很好很强大 速度非常快 花了我几十美金

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 252,800
精华内容 101,120
关键字:

pageCache