难得拥有大块的空闲时间,所以近期对操作系统的部分核心知识进行了系统的学习以及总结。借助平台将一些收获进行整理并分享给读者,希望文章中的内容能带来一些收获。
本文深入文件读写底层,引出I/O行为背后涉及到的页高速缓存、内存与块设备直接的交互、Mmap等知识点。
我们知道,当提到文件时有两个问题需要我们注意:第一是由于存储块设备与内存直接的速度不匹配从而导致的性能问题、第二个是多个进程有时需要读取同一个文件内容,即一旦数据被访问,就很有可能在短时间被二次访问。
于是便引出了我们今天要讲述的主角:页缓存(Page Cache)。
下面我们来看一个文件读取的例子:
假设系统中现在存在一个名为render的进程,该进程打开了文件scene.dat,并且每次读取其中的512B(一个扇区的大小),将读取的文件数据放入到堆分配的块中(每个进程自己的地址空间对应的物理内存)。
image1.进程调用库函数read()向内核发起读文件的请求,希望读取scene.data的512B内容;
2.这里我们假设cache中并没有所需要读取的数据所以需要向Disk中寻找数据,于是内核通过检查进程的文件描述符定位到虚拟文件系统已经打开的文件列表项,调用该文件系统对VFS的read()调用提供的接口;
3.通过文件表项链接到目录项模块,根据传入的文件路径在目录项中检索,找到该文件的inode;
4.根据inode找到文件,通过文件内容偏移量计算出要读取的页,将页中对应的512B数据先读取到page cache处;
5.最后再通过该inode的i_mapping指针找到对应的address_space页缓存树---基树,查找对应的页缓存节点;
简单来说:
-
(1)如果页缓存节点命中,那么直接返回文件内容;
-
(2)如果页缓存缺失,那么产生一个缺页异常,首先创建一个新的空的物理页框,通过该inode找到文件中该页的磁盘地址,读取相应的页填充该页缓存(DMA的方式将数据读取到页缓存),更新页表项,最后再从页缓存中读;
所有的文件内容的读取(无论一开始是命中页缓存还是没有命中页缓存)最终都是直接来源于页缓存。当将数据从磁盘复制到页缓存之后,还要将页缓存的数据通过CPU复制到read调用提供的缓冲区中,这就是普通文件IO需要的两次复制数据复制过程。其中第一次是通过DMA的方式将数据从磁盘复制到页缓存中,本次过程只需要CPU在一开始的时候让出总线、结束之后处理DMA中断即可,中间不需要CPU的直接干预,CPU可以去做别的事情;第二次是将数据从页缓存复制到进程自己的的地址空间对应的物理内存中,这个过程中需要CPU的全程干预,浪费CPU的时间和额外的物理内存空间。
此时对于render进程来说,其堆会指向Page Frames的三个存放数据的区域,从而完成数据的读取。
image在上面的内容中我们提到了——address_space与基树(radix tree)
Address_space对象
在Page Cache中存在多个不连续的物理磁盘块(512B,而页大小为4KB),所以如何通过页来定位我们所需要的数据是一个很重要并且很困难的事情。(如果能用设备号+块号来定位是最好的,但是页与块大小不匹配,所以不太行)
于是这里就需要我们引入address_space对象来帮助我们:
struct address_space {
每一个所有者(可以理解为一个具体的文件,一个inode指向的文件)对应着一个address_space对象,页高速缓存的多个页可能属于一个所有者,从而可以链接到一个address_space对象。那么一个页(page)怎么和一个address_space产生关联的呢?
page中有两个字段:mapping和index。其中mapping指向该页所有者的address_mapping(内存inode结构有一个i_mapping指向对应address_space对象),index字段表示所有者地址空间中以页大小为单位的偏移量。用这两个字段就能在页高速缓存中查找页。(这里注意一点,一个页中所包含的磁盘块在物理上不一定是相邻的)——即调用函数 find_get_page(mapping, index) 在page cache中查找所需数据。
而address_space中页表的总数由nrpages描述。
address_space中有一个host字段,该字段指向其所属的inode,也就是address_space中的host字段 与 对应inode中的 i_data字段形成互相指向的关系。
基树(Radix树)
由address_space的知识中我们知道,若需要在page cache中找到对应数据需要根据其中的radix_tree_root来找到基树的root,从而能快速搜索到所需要的值的位置。而在缓存中要求这个查询速度需要非常快,如果开销太大会抵消缓存的好处。
(Radix树就类似于传统的二叉树,如下图)
不过这里就不介绍过多,后期会专门出一篇Radix树的详细内容。
image对于写操作来说,当一个进程调用write系统调用的时候,对于文件的更新仅仅是被写到了文件的页缓存中,相应的页被标记为dirty。
在address_space中查询对应页的页缓存是否存在:如果页缓存命中,直接把文件内容修改写在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去;
如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到该文件页的磁盘地址,读取相应的页填充页缓存。
由于缓存,写操作实际上会被延迟。而那些在页缓存中修改过的数据叫做脏数据,而这些数据必须要被写回磁盘,那什么时候写的?
1 当空闲内存<阈值;
2 脏页停留时间>一定阈值时;
这里就需要调用Flusher线程进行刷新操作了;而操作系统会定时调用这个线程。
Mmap
在一次文件读取的过程中,必须将文件的内容从页缓存拷贝到用户的空间。这个过程和缺页异常(通过DMA调入需要的页)不一样,这个拷贝过程需要通过CPU进行,因此浪费了CPU的时间。另一个弊端就是浪费了物理内存,因为需要为同样的数据在内存中维护两个副本,如下图render进程的heap所对应的堆中的数据和页缓存中的数据存在重复,并且如果系统中有多个这样的进程的话,那么需要为每个进程维护同样的一份数据副本,严重浪费了CPU的时间和物理内存空间。
image于是此处需要Mmap方案。通过mmap,进程不但可以直接操作文件对应的物理内存,减少从内核空间到用户空间的数据复制过程,同时可以和别的进程共享页缓存中的数据,达到节约内存的作用。(如下图中就存在多余的数据,浪费了空间)
image对于写操作来说,普通的IO操作需要将写的数据从自己的进程地址空间复制到页缓存中,完成对页缓存的写入;但是mmap通过虚拟地址可以直接完成对页缓存的写入,减少了从用户空间到页缓存的复制。
普通文件IO中所有的文件内容的读取(无论一开始是命中页缓存还是没有命中页缓存)最终都是直接来源于页缓存。当将数据通过缺页中断从磁盘复制到页缓存之后,还要将页缓冲的数据通过CPU复制到read调用提供的缓冲区中。这样,必须通过两次数据拷贝过程,才能完成用户进程对文件内容的获取任务。写操作也是一样的,待写入的buffer在用户空间,必须将其先拷贝到内核空间对应的主存中,再写回到磁盘中,也是需要两次数据拷贝。mmap的使用减少了数据从用户空间到页缓存的复制过程,提高了IO的效率,尤其是对于大文件而言;对于比较小的文件而言,由于mmap执行了更多的内核操作,因此其效率可能比普通的文件IO更差。
mmap和常规文件操作的区别
我们首先简单的回顾一下常规文件系统操作(调用read/fread等类函数)中,函数的调用过程:
-
1、进程发起读文件请求。
-
2、内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。
-
3、inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回文件页的内容。
-
4、如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。
总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不同的繁琐过程。因此mmap效率更高。
文章在阅读操作系统相关数据的基础上还参考了一些blog:
https://blog.csdn.net/icycode/article/details/80211207#flush%E5%86%85%E6%A0%B8%E7%BA%BF%E7%A8%8B
https://manybutfinite.com/post/page-cache-the-affair-between-memory-and-files/
https://blog.csdn.net/gdj0001/article/details/80136364
内核这个东西,真的有点复杂。学习与总结都是需要慢慢进行。一天过去了,我也就学了这点东西。效率还是要提高。欢迎批评指正
网友评论