一、进程的文件寻址
内核维护的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是内核与存储介质的重要缓存结构,当我们使用write()或者read()读写文件时,假如不使用O_DIRECT标志位打开文件,我们均需要经过Page Cache来帮助我们提高文件读写速度。而在MySQL的设计实现中,读写数据文件使用了O_DIRECT标志,其目的是使用自身Buffer Pool的缓存算法。在Linux内核内存的基本单元是Page,而Page Cache也驻存于物理内存,所以Page Cache的缓存基本单位也是Page,而Page Cache缓存的内容属于文件系统,所以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操作。数据在内存中的修改会引起脏页回写,从而保证数据更新到磁盘,这些都是虚拟内存技术。内存映射的一个用途是可以让多个进程共享数据。
进程内存映射区域数量限制: vm_max_map_count限制一个进程可以拥有的VMA(虚拟内存区域)的数量。虚拟内存区域是一个连续的虚拟地址空间区域。在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间的时候,这些区域将被创建。调优这个值将限制进程可拥有VMA的数量。限制一个进程拥有VMA的总数可能导致应用程序出错,因为当进程达到了VMA上限但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误。如果你的操作系统在NORMAL区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。
es示例: Elasticsearch 对各种文件混合使用了 NioFs( 注:非阻塞文件系统)和 MMapFs ( 注:内存映射文件系统)。mmapfs类型存储分片索引到文件系统上(映射到Lucene MMapDirectory)通过映射文件到内存中(MMAP)。内存映射的过程中将划分出与被映射文件大小一样的虚拟内存空间。请确保你配置的最大映射数量,以便有足够的虚拟内存可用于 mmapped 文件。这可以暂时设置:sysctl -w vm.max_map_count=262144
或者你可以在 /etc/sysctl.conf 通过修改 vm.max_map_count 永久设置它。
六、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层分层系统,从下往上依次为:
- 第一层为全部物理内存:其管理器为伙伴系统,最小管理单位为page;
- 第二层为slab page:其管理器为slab/slub,最小管理单位为2的m次幂的字节块
buff/cache
= buffers
+ cache
, cache
则包含了 Cached
和 Slab
两部分即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分配器中的缓存对象。