浅析page cache

一、进程的文件寻址

在这里插入图片描述

内核维护的3个数据结构。进程级的文件描述符表、 系统级的打开文件描述符表、文件系统的i-node表

  • (1)每个进程都有一个进程表,表中记录了文件描述符和对应的文件表指针。表项中包含的内容如下
    • a.文件描述符 fd。
    • b.指向一个进程文件表项的指针。
  • (2)内核为进程打开文件维持一张进程级的文件描述符表。每个文件表项包含:
    • a.文件描述符及打开方式
    • b.系统打开文件表索引
  • (3)内核为所有进程打开文件维持一张系统级的文件描述符表。每个文件表项包含:
    • a.文件状态标志(读、写、添写、同步和非阻塞等)
    • b.引用计数
    • c.指向该文件V节点表项的指针
    • d.当前文件偏移量
  • (4)每个打开文件(或设备)都有一个v节点结构,每个v节点结构包含:
    • a.当前文件长度
    • b.具体i节点信息

分析:

  • 在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的。

  • 进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)。这种情形可能是在调用fork()后出现的(即,进程A、B是父子进程关系),或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生。再者是不同的进程独自去调用open函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。

  • 此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(1976),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件,也会发生类似情况。

总结: 因为一个文件可以被多个进程打开,所以多个文件表对象可以指向同一个文件节点,但多个文件表对象其对应的索引节点和目录项对象肯定是惟一的。一个inode(i节点)对应一个page cache对象,一个page cache对象包含多个物理page。


二、page cache和buffer cache的区别

当涉及到文件时,OS必须解决两个严重的问题。第一个是相对于内存的高速读写,缓慢的硬盘驱动器,特别是磁道寻找较为耗时。第二个是需要在物理内存中加载一次文件内容,并在程序之间共享这些内容。CPU如果要访问外部磁盘上的文件,需要首先将这些文件的内容拷贝到内存中,由于硬件的限制,从磁盘到内存的数据传输速度是很慢的,如果现在物理内存有空余,可以使用空闲内存来缓存一些磁盘的文件内容,这部分用作缓存磁盘文件的内存就叫做page cache。多数文件系统的默认IO操作都是缓存IO、在Linux的缓存IO机制中、操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中、也就是说、数据会先被拷贝到操作系统内核的缓冲区中、然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

page cache: 页缓冲/文件缓冲,通常4K,由若干个磁盘块组成(物理上不一定连续),也即由若干个bufferCache组成。 Page cache 也叫页缓冲或文件缓冲,是由好几个磁盘块构成,大小通常为4k,在64位系统上为8k,构成的几个磁盘块在物理磁盘上不一定连续,文件的组织单位为一页, 也就是一个page cache大小,文件读取是由外存上不连续的几个磁盘块,到buffer cache,然后组成page cache,然后供给应用程序。 Page cache在linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。具体说是加速对文件内容的访问,buffer cache缓存文件的具体内容——物理磁盘上的磁盘块,这是加速对磁盘的访问。

buffer cache: Buffer cache 也叫块缓冲,是对物理磁盘上的一个磁盘块进行的缓冲,其大小为通常为1k,磁盘块也是磁盘的组织单位。设立buffer cache的目的是为在程序多次访问同一磁盘块时,减少访问时间。Buffer cache 是由物理内存分配,linux系统为提高内存使用率,会将空闲内存全分给buffer cache ,当其他程序需要更多内存时,系统会减少cahce大小。

区别: 磁盘的操作有逻辑级(文件系统)和物理级(磁盘块),这两种Cache就是分别缓存逻辑和物理级数据的。假设我们通过文件系统操作文件,那么文件将被缓存到Page Cache,如果需要刷新文件的时候,Page Cache将交给Buffer Cache去完成,因为Buffer Cache就是缓存磁盘块的。也就是说,直接去操作文件,那就是Page Cache区缓存,用dd等命令直接操作磁盘块,就是Buffer Cache缓存的东西。在Linux2.4中,buffer cache和 page cache之间是独立的,前者使用老版本的buffer_head进行存储,这导致了一个磁盘block可能在两个cache中同时存在,造成了内存的浪费。2.6内核中将两者合并到了一起,使buffer_head只存储buffer-block的映射信息,不再存储block的内容。这样保证一个磁盘block在内存中只会有一个副本,减少了内存浪费。在linux kernel 2.4之前,这两个cache是不同的:file在page cache中,disk block在buffer cache中。考虑到大多数的文件是由disk上的文件系统表示的,数据会在系统中有两份缓存,一份在page cache中,一份在buffer cache中。许多类unix系统都是这种模式。这种实现是很简单,但是明显的低效和冗余。在linux kernel 2.4之后,这两种caches统一了。虚拟内存子系统通过page cache驱动IO。如果数据同时在page cache和buffer cache中,buffer cache会简单的指向page cache,数据只会在内存中保有一份。Page cache就是你所知道的disk在内存中的缓存:它会加快文件访问速度。

Page cache实际上是针对文件系统的,是文件的缓存,在文件层面上的数据会缓存到page cache。文件的逻辑层需要映射到实际的物理磁盘,这种映射关系由文件系统来完成。当page cache的数据需要刷新时,page cache中的数据交给buffer cache,但是这种处理在2.6版本的内核之后就变的很简单了,没有真正意义上的cache操作。Buffer cache是针对磁盘块的缓存,也就是在没有文件系统的情况下,直接对磁盘进行操作的数据会缓存到buffer cache中,例如,文件系统的元数据都会缓存到buffer cache中。简单说来,page cache用来缓存文件数据,buffer cache用来缓存磁盘数据。在有文件系统的情况下,对文件操作,那么数据会缓存到page cache,如果直接采用dd等工具对磁盘进行读写,那么数据会缓存到buffer cache。 一个inode(i节点)对应一个page cache对象,一个page cache对象包含多个物理page。


三、page cache的查找

在这里插入图片描述

Trie前缀树, 广泛应用于字符串搜索. 它是多叉树结构, 每个节点代表一个字符, 从根出发到叶子, 所访问过的字符连接起来就是一个字符串。Radix tree 是Trie的一种优化方式, 对空间进一步压缩。假如树中的一个节点是父节点的唯一子节点(the only child)的话,那么该子节点将会与父节点进行合并,这样就使得radix tree中的每一个内部节点最多拥有r个孩子, r为正整数且等于2^n(n>=1)。

查找: 如何查找需要的数据是不是已经缓存在page cache中?基树能快速根据文件索引节点和文件内的逻辑偏移快速确定指定页面是否缓存,并且能返回当前的页面状态。内核中具体的查找函数是find_get_page(mapping, offset),如果在page cache中没有找到,就会触发缺页异常page fault,调用__page_cache_alloc()在内存中分配若干物理页面,然后将数据从磁盘对应位置copy过来。内存查找数据使用了基树的数据结构。Linux radix树最广泛的用途是用于内存管理,结构address_space通过radix树跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirty或writeback的页。


四、page cache与用户/内核缓冲区的关系

在这里插入图片描述

程序在使用ssize_t write(int fd, void *buf, size_t nbytes)时,首先往用户缓冲区buffer(Userspace Page)写入数据; 然后buffer中的数据拷贝到内核缓冲区(Pagecache Page),如果内核缓冲区中还没有这个Page,就会发生Page Fault会去分配一个Page; 拷贝结束后该Pagecache Page是一个Dirty Page(脏页),然后该Dirty Page中的内容会同步磁盘,同步到磁盘后,该Pagecache 变为Clean Page并继续存在系统中。

程序在使用size_t read(int fildes,void *buf,size_t nbytes);读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。(用较小的次数填满buffer)。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。所以说:用户缓冲区的目的是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。除了在进程中设计缓冲区,内核也有自己的缓冲区。


四、page cache与直接IO

在这里插入图片描述

O_DIRECT:如果进程在open()一个文件的时候指定flags为O_DIRECT,那进程和这个文件的数据交互就直接在用户提供的buffer和磁盘之间进行,page cache就被bypass了,这种文件访问方式被称为direct I/O,适用于用户使用自己设备提供的缓存机制的场景,比如某些数据库应用。Linux允许应用程序在执行磁盘IO时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备,称为直接IO(direct IO)或者裸IO(raw IO)。但是对于某些特殊的应用程序来说、避开操作系统内核缓冲区而直接在应用程序地址空间和磁盘之间传输数据会比使用操作系统内核缓冲区获取更好的性能、自缓存应用程序就是其中的一种、自缓存应用程序会有它自己的数据缓存机制、比如它会将数据缓存在应用程序地址空间、这类应用程序完全不需要使用操作系统内核中的高速缓冲存储器、**数据库管理系统是这类应用程序的一个代表。**直接IO中数据均直接在用户地址空间(用户空间)和磁盘之间直接进行传输、完全不需要页缓存的支持。以下情况下我们可能需要考虑DirectIO:对数据写的可靠性要求很高,必须确保数据落到磁盘上,业务逻辑才可以继续执行。特定场景下,系统自带缓存算法效率不高,应用层自己实现出更高的算法。

直接IO优点:减少一次从内核缓冲区到用户程序缓存的数据复制、这种访问文件的方式通常是在对数据的缓存管理由应用程序实现的数据库管理系统中、应用程序明确地知道应该缓存哪些数据、应该失效哪些数据、操作系统只是简单地缓存最近一次从磁盘读取的数据、

直接IO缺点: 如果访问的数据不在应用程序缓存中、那么每次访问数据都会直接从磁盘加载、这种直接加载会非常缓慢。


五、mmap与直接IO

在这里插入图片描述

内存映射方式是指操作系统将内存中的某一块区域与磁盘中的文件关联起来、当要访问内存中一段数据时、转换为访问文件的某一段数据、这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作、因为这两个空间的数据是共享的。

mmap和直接io的区别: 无论mmap还是buffer IO都是向page cache中写入内容,只是写page cache的方式不一样,最终page cache的内容同步到磁盘的机制都是一样的。direct IO只是不经过page cache,直接写磁盘了而已。直接IO是不经过Page cache的,多用于数据库,直接IO终归还是IO操作,一般经过文件系统(不是一定)向设备IO请求读写。而mmap首先是虚拟内存的技术,用来创建新的虚拟存储器区域。内存映射是将文件的磁盘空间映射到一块虚拟内存的地址上,之后对这段区域的读写完全是对内存的操作而不是read/write操作。数据在内存中的修改会引起脏页回写,从而保证数据更新到磁盘,这些都是虚拟内存技术。内存映射的一个用途是可以让多个进程共享数据。


六、free命令

在这里插入图片描述

其中里面的cached: 指的就是page cache ,buffers:buffer cache。

cacheline: pageCache 是操作系统对磁盘 io 的缓存优化;cacheLine 是 cpu 对内存 io 的缓存优化,;pageCache是内存与硬盘的;cacheLine是cpu与内存之间的。

slab和pagecache:Linux中的buddy分配器是以page frame为最小粒度的,而现实的应用多是以内核objects(比如描述文件的"struct inode")的大小来申请和释放内存的,这些内核objects的大小通常从几十字节到几百字节不等,远远小于一个page的大小。那可不可以把一个page frame再按照buddy的原理,以更小的尺寸(比如128字节,256字节)组织起来,形成一个二级分配系统. linux kernel 通过slab来实现对小于page大小的内存分配。slab把page按2的m次幂进行划分一个个字节块,当kmalloc申请内存时,通过slab管理器返回需要满足申请大小的最小空闲内存块。kernel的内存管理是个2层分层系统,从下往上依次为:

  1. 第一层为全部物理内存:其管理器为伙伴系统,最小管理单位为page;
  2. 第二层为slab page:其管理器为slab/slub,最小管理单位为2的m次幂的字节块

buff/cache = buffers + cache, cache 则包含了 CachedSlab 两部分即Cache 是内核页缓存与Slab 用到的内存。 而Slab 本身也是可回收的(除去正在被使用的部分)


七、page cache的回收

回收page释放内存空间,此时会选择合适的page进行释放,如果是脏页会先同步到磁盘然后释放。触发脏页回写到磁盘时机如下:

  • 用户进程调用sync() 和 fsync()系统调用;
  • 空闲内存低于特定的阈值(threshold);
  • Dirty数据在内存中驻留的时间超过一个特定的阈值。

如何选择置换的cache页呢?Linux使用的策略是基于LRU改进的Two-List策略:

Two-List策略维护了两个list,active list 和 inactive list。在active list上的page被认为是hot的,不能释放。只有inactive list上的page可以被释放的。首次缓存的数据的page会被加入到inactive list中,已经在inactive list中的page如果再次被访问,就会移入active list中。两个链表都使用了伪LRU算法维护,新的page从尾部加入,移除时从头部移除,就像队列一样。如果active list中page的数量远大于inactive list,那么active list头部的页面会被移入inactive list中,从而位置两个表的平衡。

在系统中除了内存将被耗尽的时候可以清缓存以外,我们还可以使用下面这个文件来人工触发缓存清除的操作:

[root@tencent64 ~]# cat /proc/sys/vm/drop_caches 
1

方法是:

echo 1 > /proc/sys/vm/drop_caches

当然,这个文件可以设置的值分别为1、2、3。它们所表示的含义为:

echo 1 > /proc/sys/vm/drop_caches:表示清除pagecache。
echo 2 > /proc/sys/vm/drop_caches:表示清除回收slab分配器中的对象(包括目录项缓存和inode缓存)。slab分配器是内核中管理内存的一种机制,其中很多缓存数据实现都是用的pagecache。
echo 3 > /proc/sys/vm/drop_caches:表示清除pagecache和slab分配器中的缓存对象。

猜你喜欢

转载自blog.csdn.net/u014618114/article/details/112239886