mmap基本概念mmap是一种内存映射文件的方法,即将一个文件或其他对象映射到进程的地址空间,并在内存中创建一个虚拟段文件磁盘地址和进程虚拟地址空间的实现。地址的一对一映射。实现了这样的映射关系后,进程就可以使用指针对这段内存进行读写,系统会自动将脏页回写到对应的文件盘中,即不调用read就完成了对文件的操作,write等系统调用函数。相反,内核空间对这个区域的修改也直接反映到用户空间,这样就可以实现不同进程之间的文件共享。如下图所示:从上图可以看出,一个进程的虚拟地址空间是由多个虚拟内存区组成的。虚拟内存区域是进程虚拟地址空间中的同质区域,即具有相同特征的连续地址范围。上图所示的文本数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射都是独立的虚拟内存区域。内存映射的地址空间在栈之间的空闲部分。Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域。由于各个异构虚拟内存区的功能和内部机制不同,因此一个进程使用多个vm_area_struct结构来表示不同类型的虚拟内存区。每个vm_area_struct结构都使用链表或者树结构链接,方便进程快速访问,如下图所示:可以导致该区域可以使用的所有系统调用函数。这样就可以从vm_area_struct中获取进程对某个虚拟内存区域进行任何操作所需要的信息。mmap的作用是新建一个vm_area_struct结构,连接到文件的物理磁盘地址。具体步骤见下一节。mmap内存映射的原理mmap内存映射的实现过程大体上可以分为三个阶段:(1)进程启动映射进程,在虚拟地址空间创建一个虚拟映射区用于映射1.进程调用用户空间函数mmap,原型:void*mmap(void*start,size_tlength,intprot,intflags,intfd,off_toffset);2、在当前进程的虚拟地址空间中,找到一个空闲的连续的符合要求的虚拟地址。3.为此虚拟区分配一个vm_area_struct结构,然后初始化这个结构的各个字段4.将新创建的虚拟区结构(vm_area_struct)插入到进程的虚拟地址区列表或树中(2)调用系统调用内核空间的mmap函数(区别于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系5.为映射分配新的虚拟地址区后,通过要映射的文件指针在文件描述符表中找到对应的文件描述符,通过文件描述符链接到内核“打开的文件集”中文件的文件结构(structfile),而每个文件结构维护与打开的文件相关的各种信息。6、通过文件的文件结构链接到file_operations模块,调用内核函数mmap,其原型为:intmmap(structfile*filp,structvm_area_struct*vma),区别于用户空间库函数。7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘的物理地址。8、通过remap_pfn_range函数创建页表,实现文件地址与虚拟地址区域的映射关系。此时,这块虚拟地址没有任何与主存关联的数据。(3)进程发起对这个映射空间的访问,引发缺页异常,实现了将文件内容复制到物理内存(主存)中的任意文件数据到主存的复制。真正的文件读取是在进程启动读取或写入操作时。9、进程的读或写操作访问的是虚拟地址空间的映射地址。通过查询页表,发现这个地址不在物理页上。因为目前只建立了地址映射,还没有将真正的硬盘数据复制到内存中,所以导致了pagefault异常。10.针对pagefault异常进行一系列的判断。确认没有非法操作后,内核发起请求分页过程。11、分页进程首先在交换缓存空间(swapcache)中寻找需要访问的内存页面,如果没有页面,则调用nopage函数将缺失的页面从磁盘加载到主存中。12.之后,进程就可以读写这块主存了。如果写操作改变了它的内容,系统会在一定时间后自动将脏页回写到相应的磁盘地址,即文件写入完成。过程。注意:修改后的脏页不会立即更新回文件,而是会有一段时间的延迟。可以调用msync()强制同步,这样写入的内容可以立即保存到文件中。mmap和常规文件操作的区别不了解Linux文件系统的朋友可以参考我之前的博文《从内核文件系统看文件读写过程》。先简单回顾一下常规的文件系统操作函数(调用read/fread等函数)。调用流程:1.进程发起文件读取请求。2、内核通过查找进程文件符号表,??在内核打开的文件集上定位文件信息,从而找到这个文件的inode。3、inode查找要请求的文件页是否已经缓存在address_space上的pagecache中。如果存在,则直接返回该文件页面的内容。4、如果不存在,则通过inode定位文件的磁盘地址,将数据从磁盘复制到pagecache中。然后再次发起页面读取进程,然后将页面缓存中的数据发送给用户进程。综上所述,常规的文件操作为了提高读写效率和保护磁盘,都会使用pagecache机制。这样,在读取文件时,需要先将文件页从磁盘复制到页缓存中。由于pagecache在内核空间,不能被用户进程直接寻址,所以需要将pagecache中的数据页再次复制到对应的用户内存中。在太空。这样经过两次数据拷贝过程就可以完成进程获取文件内容的任务。写操作也是如此。要写入的缓冲区不能在内核空间中直接访问。必须先拷贝到内核空间对应的主存中,然后再写回磁盘(延迟回写),同样需要两次数据拷贝。在使用mmap操作文件时,在创建新的虚拟内存区和建立文件磁盘地址与虚拟内存区映射这两个步骤中没有文件复制操作。然后在访问数据时,发现内存中没有数据,启动异常pagefault流程。通过建立的映射关系,只使用一份数据,将数据从磁盘转移到内存的用户空间供进程使用。使用。总而言之,常规文件操作需要从磁盘到页面缓存再到用户主存的两份数据副本。而mmap操作的是文件,从磁盘到用户主存只需要一次数据copy过程。说白了,mmap的关键点就是实现用户空间和内核空间之间数据的直接交互,而无需经历不同空间之间数据通信的繁琐过程。所以mmap效率更高。mmap优点总结从上面的讨论我们可以看出mmap的优点有以下几点:1.文件的读操作跨越了pagecache,减少了数据的拷贝次数,取代了I/O读写内存读写,提高文件读取效率。2、实现用户空间和内核空间的高效交互。两个空间各自的修改操作可以直接反映在映射区域中,从而及时被对方空间捕获。3、提供进程间共享内存和通信的方式。不管是父子进程还是无关进程,都可以将自己的用户空间映射到同一个文件或者匿名映射到同一个区域。这样就可以通过映射区的各自变化来达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射区域C,当A第一次读取C时,通过缺页将文件页从磁盘复制到内存;但是当B再次读取C的同一页时,虽然会出现pagefault异常,但是已经不需要再从磁盘中拷贝文件,而是可以直接使用已经存在内存中的文件数据。4、可用于实现高效的大规模数据传输。内存空间不足是制约大数据运行的一方面。解决办法往往是使用硬盘空间来辅助运行,补充内存的不足。但更进一步,会造成大量的文件I/O操作,大大影响效率。这个问题可以通过mmap映射很好的解决。也就是说,每当你需要使用磁盘空间而不是内存时,mmap就可以发挥它的作用。mmap相关函数的函数原型为void*mmap(void*start,size_tlength,intprot,intflags,intfd,off_toffset);return表示执行成功时,mmap()返回映射区域的指针。失败时,mmap()返回MAP_FAILED[其值为(void*)-1],错误设置为以下值之一:返回错误类型参数start:映射区的起始地址length:映射区的长度mappingareaprot:expected文件的内存保护标志不能和文件的打开方式冲突。是以下值之一,可以通过OR运算合理组合protflags:指定映射对象的类型,映射选项和映射页面可以共享。它的值可以是以下一个或多个位的组合flagfd:一个有效的文件描述符。如果设置了MAP_ANONYMOUS,它的值应该是-1offset,因为兼容性问题:当映射对象内容起始点相关的函数intmunmap(void*addr,size_tlen)执行成功后,munmap()返回0。失败时,munmap返回-1,错误返回标志与mmap一致;本次调用释放进程地址空间中的一个映射关系,addr为调用mmap()时返回的地址,len为映射区域的大小;当映射关系解除后,访问原映射地址会导致段错误。intmsync(void*addr,size_tlen,intflags)一般来说,映射空间中进程共享内容的改变不会直接写回磁盘文件,往往是在调用munmap()之后进行操作.通过调用msync()可以使磁盘上的文件内容与共享内存区的内容保持一致。mmap使用细节1、使用mmap需要注意的一个关键点是mmap映射区的大小必须是物理页面大小(page_size)的整数倍(在32位系统中通常为4k字节)。原因是内存的最小粒度是页,进程虚拟地址空间和内存的映射也是以页为单位的。mmap为了配合对内存的操作,从磁盘到虚拟地址空间的映射也必须是一个page。2、内核可以跟踪内存映射底层对象(文件)的大小,进程可以合法访问当前文件大小和内存映射区域内的那些字节。也就是说,如果文件大小不断扩大,只要数据在映射区范围内,进程就可以合法获取,与建立映射时文件大小无关.详见“案例三”。3、映射建立后,即使关闭文件,映射仍然存在。因为映射的是磁盘的地址,而不是文件本身,所以与文件句柄无关。同时,进程间通信可用的有效地址空间并不完全受映射文件大小的限制,因为它是按页映射的。在了解了以上知识的前提下,我们再来看看size不是page的整数倍时的具体情况:情况一:一个文件的大小为5000字节,mmap函数从的起始位置开始一个文件并将5000字节映射到虚拟内存中。分析:因为单位物理页大小为4096字节,映射的文件虽然只有5000字节,但是进程虚拟地址区对应的大小需要满足整个页大小,所以执行mmap函数后,实际上是映射的到虚拟内存区8192字节,5000~8191字节用0填充。映射后的对应关系如下图所示:此时:(1)读/写前5000字节(0~4999),返回操作文件的内容。(2)读5000~8191字节时,结果全为0。写5000~8191时,过程不会报错,但写入的内容不会写入原文件。(3)读写8192以外的磁盘部分时,会返回SIGSECV错误。场景二:一个文件的大小是5000字节,mmap函数从一个文件的起始位置开始,映射15000字节到虚拟内存,即映射的大小超过了原文件的大小。分析:由于文件大小为5000字节,与案例一对应两个物理页。那么这两个物理页是合法的,可以读写,但是超过5000的部分不会在原文件中体现出来。由于程序需要映射15000字节,而文件只占用两个物理页,所以8192字节到15000字节无法读写,运行时会返回异常。如下图所示:此时:(1)进程可以正常读/写映射的前5000字节(0~4999),写操作的变化会在经过一段时间后反映到原文件中一定时期。(2)对于5000~8191字节,进程可以无误读写。但是写入前内容全为0,写入后不会反映到文件中。(3)对于8192~14999字节,进程无法读写,会报SIGBUS错误。(4)对于15000以外的字节,进程无法读写,会触发SIGSEGV错误。场景三:一个文件初始大小为0,使用mmap操作映射1000*4K的大小,即1000个物理页约4M字节,mmap返回指针ptr。分析:如果文件是在映射建立之初读写的,由于文件大小为0,没有合法的物理页对应,如情况2,会返回SIGBUS错误。但是,如果每次ptr读写操作前都增加了文件大小,那么ptr在文件大小范围内的操作是合法的。比如文件扩展到4096字节,ptr可以操作ptr~[(char)ptr+4095]的空间。只要文件扩展范围在1000个物理页(映射范围)以内,ptr就可以对应同大小的操作。这样方便随时扩展文件空间,随时写入文件,不会造成空间浪费。
