内存映射是linux中的一个重要机制,它和虚拟内存管理和文件IO都有直接的关系,本篇将详细介绍linux中内存映射的原理。
mmap基本概念
在介绍内存映射之前,首先知道现代计算机系统普遍采用虚拟内存的方式管理物理内存。在32位机器上每个进程都有自己的4G虚拟内存空间,其中0-3G属于用户空间,是该进程独有的;3-4G之间的是内核空间,是计算机中的所有进程的内核空间和内核进程所共享的地址空间。必须明确的一点是:用户空间和内核空间属于虚拟地址空间,是虚拟内存中的概念。进程地址空间的划分如图1所示。
图 1 进程地址空间
mmap将就是图1的进程空间中用户空间的内存映射区域和磁盘上的某个文件或者其他对象形成一一对应关系,形成这样的关系之后,进程就可以采用对虚拟地址(指针)读写的方式实现对内存映射区对应的物理内存的读写,而这种读写会被系统自动通过后台线程(flusher)刷到后备存储空间、或者通过msync调用刷到对应的后备存储空间,从而不用调用read(),write()等系统调用实现对文件的读写。如果不同的进程映射同一个文件的同样区间或者映射同一个匿名对象到各自的虚拟内存空间,那么可以实现进程之间的通信,这是一种十分高效的IPC方式。
从图1中可以看出进程虚拟地址空间中,用户空间被划分成很多的段,包含代码段、数据段、未初始化数据段、堆、内存映射区还有栈等区间,每一个区间都对应一个或者多个管理结构体,在内核中用vm_area_struct表示。那么进程的用户空间可以表示为vm_area_struct结构体组成的链表,如图2所示。
![](https://i-blog.csdnimg.cn/blog_migrate/ff9f807823b70f487b57494490815e22.png)
图 2 进程地址空间中不同区段在内核中的表示
mmap的基本原理
mmap内存映射的实现过程,可以分为三个阶段:
一、进程在用户空间调用mmap启动映射过程,内核在该进程的虚拟地址空间中为映射创建虚拟映射区域结构体---vm_area_struct
1.进程在用户空间调用mmap,函数原型为
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
关于该系统调用的中多解释可以参考《unix环境高级编程》中14.8节;
2.如果start为NULL(一般的使用情况,增强可移植性),则由内核在当前进程的虚拟地址空间中,寻找一段能够满足映射长度需求的连续虚拟地址区间;
3.为寻找到的连续虚拟地址区间分配一个vm_area_struct结构体,接着对这个结构体中的各个成员变量进行初始化;
4.将新建的虚拟区间结构体插入到当前进程的vm_area_struct结构体链表和相应的红黑树结构中;
二、调用内核空间的系统函数mmap(与为用户空间提供的系统调用不是一回事),实现文件磁盘地址和进程虚拟地址的一一映射关系
1.为映射分配了新的虚拟地址区域之后,通过待映射的文件描述符在“进程文件描述符表”的struct file* fd_array中找到对应的指向文件的struct file*,每个进程都为该进程打开的所有文件保存着一个struct file*数组,其中的每个元素都是指向“系统文件描述符”的中的struct file的。系统为每个被打开的文件都维护一个struct file,struct file中存在指向已打开文件的struct path成员,struct path成员中存在指向系统打开的特定文件的struct dentry*,struct dentry存在指向该能够唯一标识该文件的struct inode结构体(这种关系可以参考博客),简化图如图3。
![](https://i-blog.csdnimg.cn/blog_migrate/f6762ff3958e9bcbd9b959a0917362c2.png)
图 3 进程地址空间、页缓存和文件系统关系图
2.通过该文件最终找到的struct inode结构体中的struct file_operations* i_fop模块,调用内核函数的mmap(也就是驱动程序,该模块也是虚拟文件系统和实际文件系统连接的桥梁),其原型为int mmap(struct file* filp, struct vm_area_struct* vma),通过该内核函数就知道这是将虚拟地址区间和实际的后备文件系统相关联。
3.通过1中所述,由struct file能够找到系统打开的文件所对应的inode结构体,并通过inode结构体中的设备号和块号最终定位到文件在磁盘的物理地址。
4.通过remap_pfn_rang函数建立页表,实现文件在磁盘中的地址和虚拟地址区域的映射关系。此时这片虚拟地址并没有任何数据关联到主存中,只是建立了虚拟地址和磁盘地址之间的映射关系。
三、进程访问分配的虚拟的地址区间中的某个地址,引发缺页异常,实现文件内容到物理内存的拷贝
1.进程的读或者写操作访问虚拟地址区间中的一个或者一段映射地址,通过查询页表,发现这一段地址并不在主存上存在对应的物理页面。因为目前只是建立了地址映射,真正的硬盘数据还没有拷贝到物理内存中,由此引发缺页异常。
2.缺页异常通过一系列判断,确定操作合法后,通过DMA的方式读取数据。
3.调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到物理内存中。这个过程还涉及分配物理页框、确定物理页框的地址、将读取的数据写入物理页框,最终更新页表中访问的虚拟地址对应的物理页框的地址。
4.之后进程从缺页异常中恢复即可读取该内容文件对应的实际内容或者对文件进行写操作。
mmap调用需要说明的一点是物理页框最终是由内核以页缓存的方式进行管理的,参见《linux内核设计与实现》中页缓存一章。因此,如果是对文件进行写操作,那么脏页面不会立即更新到文件中,而是暂时写到页缓存中,由后台的flusher线程将对文件的更新写到磁盘中或者是由用户调用msync将对应的数据刷新到磁盘中。
mmap映射的四种类型
mmap分为有后备文件的映射和匿名映射,而这两种映射又都有私有映射和共享映射之分,所以mmap一共存在四种类型的映射。
1.有后备文件的共享映射。多个进程的vm_area_struct指向同一个物理内存区域,一个进程对文件内容的修改对其他进程可见,对文件内容的修改最终会被写到后备文件中。
2.有后备文件的私有映射。多个进程的vm_area_struct指向同一个物理内存区域,采用写时拷贝的方式,当一个进程对文件内容做修改,不会被其他的进程所看到,另外对文件内容的修改也不会被写到后备文件。当内存不够需要执行页回收操作的时候,私有映射的页被交换到交换区或者直接写到磁盘。一般用在加载共享代码库。
3.匿名文件的共享映射。内核创建一个都是0的物理内存区域,然后多个进程的vm_area_struct指向这个共享的物理内存区域,对该区域内容的修改对所有的进程都是可见的,匿名文件在页会页回收的时候被交换到交换区。
4.匿名文件的私有映射。内核创建一个初始都是0的物理内存区域,对该区域的内容的修改只对进程创建者可见,匿名文件在页回收的时候被交换到交换区。malloc()的底层调用是用了匿名文件的私有映射来分配大块的内存。
mmap的用途
内存映射的用途很多,如
1.后备文件的共享映射可以用作内存映射IO来对大文件进行操作,比普通IO减少一次内存的拷贝工作。需要注意的是内存映射IO涉及到内核的很多操作,比如vm_area_struct的创建、页表的修改等等,比普通的IO操作更加复杂。小文件的读写使用普通IO更合适。
2.后备文件的私有映射可以用作共享库二进制文件代码段,数据段的加载。
3.匿名文件的共享映射可以用作fork时,让父子进程共享匿名映射分配的内存。
4.匿名文件的私有映射可以用作进程的私有内存分配。
以上是个人关于mmap的理解,欢迎吐槽。