普通文件IO需要复制两次,内存映射文件mmap只需要复制一次

普通文件IO进程发起读请求的过程如下:

(1)进程调用库函数read()向内核发起读文件的请求

(2)内核通过检查进程的文件描述符定位到虚拟文件系统已经打开的文件列表项,调用该文件系统对VFS的read()调用提供的接口

(3)通过文件表项链接到目录项模块,根据传入的文件路径在目录项中检索,找到该文件的inode

(4)inode中,通过文件内容偏移量计算出要读取的页

(5)通过该inode的i_mapping指针找到对应的address_space页缓存树—基数树,查找对应的页缓存节点:

1)如果页缓存节点命中,那么直接返回文件内容

2)如果页缓存缺失,那么产生一个缺页异常,首先创建一个新的空的物理页框,通过该inode找到文件中该页的磁盘地址,读取相应的页填充该页缓存(DMA的方式将数据读取到页缓存),更新页表项;重新进行第5步的查找页缓存的过程

(6)文件内容读取成功

所有的文件内容的读取(无论一开始是命中页缓存还是没有命中页缓存)最终都是直接来源于页缓存。当将数据从磁盘复制到页缓存之后,还要将页缓存的数据通过CPU复制到read调用提供的缓冲区中,这就是普通文件IO需要的两次复制数据复制过程。其中第一次是通过DMA的方式将数据从磁盘复制到页缓存中,本次过程只需要CPU在一开始的时候让出总线、结束之后处理DMA中断即可,中间不需要CPU的直接干预,CPU可以去做别的事情;第二次是将数据从页缓存复制到进程自己的的地址空间对应的物理内存中,这个过程中需要CPU的全程干预,浪费CPU的时间和额外的物理内存空间

在一次文件读取的过程中,必须将文件的内容从页缓存拷贝到用户的空间。这个过程和缺页异常(通过DMA调入需要的页)不一样,这个拷贝过程需要通过CPU进行,因此浪费了CPU的时间。另一个弊端就是浪费了物理内存,因为需要为同样的数据在内存中维护两个副本,并且如果系统中有多个这样的进程的话,那么需要为每个进程维护同样的一份数据副本,严重浪费了CPU的时间和物理内存空间

通过内存映射IO—mmap,进程不但可以直接操作文件对应的物理内存,减少从内核空间到用户空间的数据复制过程,同时可以和别的进程共享页缓存中的数据,达到节约内存的作用

当映射一个文件到内存中的时候,内核将虚拟地址直接映射到页缓存中。当映射一个文件的时候,如果文件的内容不在物理内存中,操作系统不会将所映射的文件部分的全部内容直接拷贝到物理内存中,而是在使用虚拟地址访问物理内存的时候通过缺页异常将所需要的数据调入内存中。如果文件本身已经存在于页缓存中,则不再通过磁盘IO调入内存

由于页缓存的架构,当一个进程调用write系统调用的时候,对于文件的更新仅仅是被写到了文件的页缓存中,相应的页被标记为dirty。具体过程如下:

前面5步和读文件是一致的,在address_space中查询对应页的页缓存是否存在:

(6)如果页缓存命中,直接把文件内容修改写在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。

(7)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到该文件页的磁盘地址,读取相应的页填充页缓存。此时缓存页命中,进行第6步

普通的IO操作需要将写的数据从自己的进程地址空间复制到页缓存中,完成对页缓存的写入;但是mmap通过虚拟地址(指针)可以直接完成对页缓存的写入,减少了从用户空间到页缓存的复制

由于写操作只是写到了页缓存中,因此进程并没有被阻塞到磁盘IO发生,因此当计算机崩溃的时候,写操作所引起的改变可能并没有发生在磁盘上。所以,对于一些要求严格的写操作,比如数据库系统,就需要调用fsync等操作及时将数据同步到磁盘上(虽然这中间也可能存在磁盘的驱动程序崩溃的情况)。读操作与写不同,一般会阻塞到进程读取到数据(除非调用非阻塞IO,即使使用IO多路复用技术也是将进程阻塞在多个监听描述符上,本质上还是阻塞的)。为了减轻读操作的这种延迟,linux操作系统的内核使用了”预读”技术,也就是当从磁盘中读取你所需要的数据的时候,内核将会多读取一些页到页缓存中

文件映射分为私有映射(private)和共享映射(shared)两种,二者之间的区别就是一个进程对文件所做的改变能否被其他的进程所看到,且能否同步到后备的存储介质中

如果两个进程只是读取文件中的内容,不做任何的改动,那么文件只在物理内存中保留一份;但是如果有一个进程,如render,要对文件中的内容做出改动,那么会触发缺页中断,内核采用写时复制技术,为要改动的内容对应的页重新分配一个物理页框,将并将被改动的内容对应的物理页框中的数据复制到新分配的物理页框中,再进行改动。此时新分配的物理页框对于render而言是它自己“私有的”,别的进程是看不到的,也不会被同步到后备的存储中。但是如果是共享映射,所有的进程都是共享同一块页缓存的,此时被映射的文件的数据在内存中只保留一份。任何一个进程对映射区进行读或者写,都不会导致对页缓冲数据的复制。

mmap的系统调用函数原型为void* mmap(void* addr, size_t len, int prot, int flag, int fd, off_t off)。其中,flag指定了是私有映射还是共享映射,私有映射的写会引发缺页中断,然后复制对应的物理页框到新分配的页框中。prot指定了被映射的文件是可读、可写、可执行还是不可访问。如果prot指定的是可读,但是却对映射文件执行写操作,则此时却缺页中断会引起段错误,而不是进行写时复制。

内存映射的一个典型应用就是动态共享库的加载。