页缓存

  1. linux中页缓存的本质就是对于磁盘中的部分数据在内存中保留一定的副本,使得应用程序能够快速的读取到磁盘中相应的数据,并实现不同进程之间的数据共享。
  2. linux中页缓存的引入主要是为了解决两类重要的问题:
    (1)磁盘读写速度较慢(ms 级别);
    (2)实现不同进程之间或者同一进程的前后不同部分之间对于数据的共享
  3. 如果没有进程之间的共享机制,那么对于系统中所启动的所有进程在打开文件的时候都要将需要的数据从磁盘加载进物理内存空间,这样不仅造成了加载速度变慢(每次都从磁盘中读取数据),而且造成了物理内存的浪费。为了解决以上问题,linux操作系统使用了缓存机制。在虚拟内存机制出现以前,操作系统使用块缓存机制,但是在虚拟内存出现以后操作系统管理IO的粒度更大,因此采用了页缓存机制。此后,和后备存储的数据交互普遍以页为单位。页缓存是基于页的、面向文件的一种缓存机制。
  4. 页缓存就是将一个文件在内存中的所有物理页所组成的一种树形结构,我们称之为基数树,用于管理属于同一个文件在内存中的缓存内容。
  5. 一个文件在内存中具有唯一的inode结构标识,inode结构中有该文件所属的设备及其标识符,因而,根据一个inode能够确定其对应的后备设备。

页缓存、内存、文件IO

  1. 普通文件IO需要复制两次,内存映射文件mmap只需要复制一次
  2. 普通文件IO进程发起读请求的过程如下:
    (1)进程调用库函数read()向内核发起读文件的请求
    (2)内核通过检查进程的文件描述符定位到虚拟文件系统已经打开的文件列表项,调用该文件系统对VFS的read()调用提供的接口
    (3)通过文件表项链接到目录项模块,根据传入的文件路径在目录项中检索,找到该文件的inode
    (4)inode中,通过文件内容偏移量计算出要读取的页
    (5)通过该inode的i_mapping指针找到对应的address_space页缓存树—基数树,查找对应的页缓存节点:
    1)如果页缓存节点命中,那么直接返回文件内容
    2)如果页缓存缺失,那么产生一个缺页异常,首先创建一个新的空的物理页框,通过该inode找到文件中该页的磁盘地址,读取相应的页填充该页缓存(DMA的方式将数据读取到页缓存),更新页表项;重新进行第5步的查找页缓存的过程
    (6)文件内容读取成功
  3. 所有的文件内容的读取(无论一开始是命中页缓存还是没有命中页缓存)最终都是直接来源于页缓存。当将数据从磁盘复制到页缓存之后,还要将页缓存的数据通过CPU复制到read调用提供的缓冲区中,这就是普通文件IO需要的两次复制数据复制过程。其中第一次是通过DMA的方式将数据从磁盘复制到页缓存中,本次过程只需要CPU在一开始的时候让出总线、结束之后处理DMA中断即可,中间不需要CPU的直接干预,CPU可以去做别的事情;第二次是将数据从页缓存复制到进程自己的的地址空间对应的物理内存中,这个过程中需要CPU的全程干预,浪费CPU的时间和额外的物理内存空间
  4. 在一次文件读取的过程中,必须将文件的内容从页缓存拷贝到用户的空间。这个过程和缺页异常(通过DMA调入需要的页)不一样,这个拷贝过程需要通过CPU进行,因此浪费了CPU的时间。另一个弊端就是浪费了物理内存,因为需要为同样的数据在内存中维护两个副本,并且如果系统中有多个这样的进程的话,那么需要为每个进程维护同样的一份数据副本,严重浪费了CPU的时间和物理内存空间
  5. 通过内存映射IO—mmap,进程不但可以直接操作文件对应的物理内存,减少从内核空间到用户空间的数据复制过程,同时可以和别的进程共享页缓存中的数据,达到节约内存的作用
  6. 当映射一个文件到内存中的时候,内核将虚拟地址直接映射到页缓存中。当映射一个文件的时候,如果文件的内容不在物理内存中,操作系统不会将所映射的文件部分的全部内容直接拷贝到物理内存中,而是在使用虚拟地址访问物理内存的时候通过缺页异常将所需要的数据调入内存中。如果文件本身已经存在于页缓存中,则不再通过磁盘IO调入内存
  7. 由于页缓存的架构,当一个进程调用write系统调用的时候,对于文件的更新仅仅是被写到了文件的页缓存中,相应的页被标记为dirty。具体过程如下:
    前面5步和读文件是一致的,在address_space中查询对应页的页缓存是否存在:
    (6)如果页缓存命中,直接把文件内容修改写在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。
    (7)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到该文件页的磁盘地址,读取相应的页填充页缓存。此时缓存页命中,进行第6步
  8. 普通的IO操作需要将写的数据从自己的进程地址空间复制到页缓存中,完成对页缓存的写入;但是mmap通过虚拟地址(指针)可以直接完成对页缓存的写入,减少了从用户空间到页缓存的复制
  9. 由于写操作只是写到了页缓存中,因此进程并没有被阻塞到磁盘IO发生,因此当计算机崩溃的时候,写操作所引起的改变可能并没有发生在磁盘上。所以,对于一些要求严格的写操作,比如数据库系统,就需要调用fsync等操作及时将数据同步到磁盘上(虽然这中间也可能存在磁盘的驱动程序崩溃的情况)。读操作与写不同,一般会阻塞到进程读取到数据(除非调用非阻塞IO,即使使用IO多路复用技术也是将进程阻塞在多个监听描述符上,本质上还是阻塞的)。为了减轻读操作的这种延迟,linux操作系统的内核使用了”预读”技术,也就是当从磁盘中读取你所需要的数据的时候,内核将会多读取一些页到页缓存中
  10. 文件映射分为私有映射(private)和共享映射(shared)两种,二者之间的区别就是一个进程对文件所做的改变能否被其他的进程所看到,且能否同步到后备的存储介质中
  11. 如果两个进程只是读取文件中的内容,不做任何的改动,那么文件只在物理内存中保留一份;但是如果有一个进程,如render,要对文件中的内容做出改动,那么会触发缺页中断,内核采用写时复制技术,为要改动的内容对应的页重新分配一个物理页框,将并将被改动的内容对应的物理页框中的数据复制到新分配的物理页框中,再进行改动。此时新分配的物理页框对于render而言是它自己“私有的”,别的进程是看不到的,也不会被同步到后备的存储中。但是如果是共享映射,所有的进程都是共享同一块页缓存的,此时被映射的文件的数据在内存中只保留一份。任何一个进程对映射区进行读或者写,都不会导致对页缓冲数据的复制。
  12. mmap的系统调用函数原型为void* mmap(void* addr, size_t len, int prot, int flag, int fd, off_t off)。其中,flag指定了是私有映射还是共享映射,私有映射的写会引发缺页中断,然后复制对应的物理页框到新分配的页框中。prot指定了被映射的文件是可读、可写、可执行还是不可访问。如果prot指定的是可读,但是却对映射文件执行写操作,则此时却缺页中断会引起段错误,而不是进行写时复制。
  13. 内存映射的一个典型应用就是动态共享库的加载。

页缓存写回磁盘

  1. 普通文件IO,都是将数据直接写在页缓存上,何时写回:
    (1)空闲内存的值低于一个指定的阈值的时候,内核必须将脏页写回到后备存储以释放内存。因为只有干净的内存页才可以回收。当脏页被写回之后就变为PG_uptodate标志,变为干净的页,内核就可以将其所占的内存回收
    (2)当脏页在内存中驻留的时间超过一个指定的阈值之后,内核必须将该脏页写回到后备存储,以确定脏页不会在内存中无限期的停留
    (3)当用户进程显式的调用fsync、fdatasync或者sync的时候,内核按照要求执行回写操作
  2. 页缓存由谁写回磁盘:
    (1)为了能够不阻塞写操作,并且将脏页及时的写回后备存储。linux在当前的内核版本中使用了flusher线程负责将脏页回写
    (2)为了满足第一个何时回写的条件,内核在可用内存低于一个阈值的时候唤醒一个或者多个flusher线程,将脏页回写
    (3)为了满足第二个条件,内核将通过定时器定时唤醒flusher线程,将所有驻留时间超时的脏页回写

内存映射

  1. 内存映射在Linux操作系统中与高效的跨进程通信&文件操作息息相关。关联进程中的1个虚拟内存区域&1个磁盘上的对象,使得二者存在映射关系被映射的对象称为:共享对象(普通文件/匿名文件)
  2. 在多个进程的虚拟内存区域 已和同1个共享对象 建立映射关系的前提下,若其中1个进程对该虚拟区域进行写操作,那么,对于也把该共享对象映射到其自身虚拟内存区域的进程 也是可见的
  3. 内存映射的实现过程主要是通过Linux系统下的系统调用函数:mmap()该函数的作用 = 创建虚拟内存区域 + 与共享对象建立映射关系
  4. 内存映射特点:
    (1)提高数据的读、写 & 传输的时间性能
    (2)减少了数据拷贝次数
    (3)用户空间 & 内核空间的高效交互(通过映射的区域直接交互)
    (4)用内存读写 代替 I/O读写
    (5)提高内存利用率:通过虚拟内存 & 共享对象
  5. 在Linux系统下,根据内存映射的本质原理 & 特点,其应用场景在于:
    (1)实现内存共享:如 跨进程通信
    (2)提高数据读/写效率 :如文件读/写操作