内存映射mmap是Linux内核的一个重要机制,它和虚拟内存管理以及文件IO都有直接的关系,这篇细说一下mmap的一些要点。

修改(2015-11-12):Linux的虚拟内存管理是基于mmap来实现的。vm_area_struct是在mmap的时候创建的,vm_area_strcut代表了一段连续的虚拟地址,这些虚拟地址相应地映射到一个后备文件或者一个匿名文件的虚拟页。一个vm_area_struct映射到一组连续的页表项。页表项又指向物理内存page,这样就把一个文件和物理内存页相映射。

来理解一下虚拟地址映射的过程:拿到一个虚拟地址,根据已有的vm_area_struct看这个虚拟地址是否属于某个vm_area_struct

  • 如果没有匹配到,就报段错误,访问了一个没有分配的虚拟地址。
  • 如果匹配到了vm_area_struct,根据虚拟地址和页表的映射关系,找到对应的页表项PTE,如果PTE没有分配,就报一个缺页异常,去加载相应的文件数据到物理内存,如果PTE分配,就去相应的物理页的偏移位置读取数据

所以虚拟页的三种状态的实际含义如下:

  • 未分配虚拟页,指的是没有使用mmap建立vm_area_struct,所以也就没有对应到具体的页表项
  • 已分配虚拟页,未映射到物理页,指的是已经使用了mmap建立的vm_area_struct,可以映射到对应的页表项,但是页表项没有指向具体的物理页
  • 已分配虚拟页,已映射到物理页,指的是已经使用了mmap建立的vm_area_struct,可以映射到对应的页表项,并且页表项指向具体的物理页

mmap要么映射到一个后备文件,要么映射到一个匿名文件。操作系统分配物理内存时实际用到了匿名文件的mmap。

mmap和虚拟内存管理

先来看看Linux内核的用户进程虚拟内存管理。内核定义了mm_struct结构来表示一个用户进程的虚拟内存地址空间。

1. start_code, end_code指定了进程的代码段的边界,start_data, end_data指定了进程数据段的边界。在ELF二进制文件映射到虚拟内存地址空间后,这几个长度就不会再改变

2. start_brk, brk指定了堆的边界。start_brk表示堆的起始地址,在进程整个生命周期不会改变,brk表示堆的结束位置,会随着堆的长度改变而改变

3. stack_top指定了栈的起始位置,一般是

4. task_size指定了用户进程地址空间的长度,也就是用户进程地址空间的顶部边界

5. mmap_base指定了用户进程虚拟地址空间中用作内存映射部分的地址的基地址,这个位置不是随机的,通常是TASK_SIZE / 3位置处

这里各个区域的地址都是用户进程的虚拟地址,用户进程使用虚拟地址和页表结构来访问内存

1. 首先根据所在区域的虚拟地址转换成对应的页表数组的数组项索引,找到页表索引最后定为到PTE中保存的物理内存页的页号,加上虚拟地址低12位的offset来确定一个唯一的物理内存地址

2. 如果物理内存地址所在的页存在,就返回该物理地址存放的内容。如果不存在就触发缺页异常。虚拟内存管理采用按需分配 + 缺页异常机制来管理页表项和分配对应的物理内存页。当一个虚拟地址对应的页表项不存在时,先创建页表结构,再分配物理内存页,再修改页表

进程的mm_struct除了包含虚拟内存地址空间布局的信息,还包含了虚拟内存区域vm_area_struct的信息。虚拟内存区域vm_area_struct是内核管理用户进程虚拟地址空间的方式,实际上数据段,文本段,共享库这些都是通过vm_area_struct来管理的

vm_area_struct由两种组织形式,一种是单链表,包含了所有创建的vm_area_struct实例,一种是红黑树结构,加速区域的查找。这两个数据结构都是面向同一份vm_area_struct实例,只是组织形式不同。

再考虑一下vm_area_struct和页表的关系,vm_area_struct本质上是一段用户进程的虚拟地址,而我们知道虚拟地址和页表数组的索引是对应的,页表数组的最后一级PTE数组的数组项存放着物理内存页的页号,这样就建立了虚拟内存地址到物理内存地址的对应关系。

1. 有一种情况是先有虚拟地址,再由访问虚拟地址引起缺页异常去加载物理内存,再更新页表建立虚拟地址,页表,物理内存三者的联系

2. 有一种情况(mmap)是先从设备加载文件,建立address_space,页缓存(物理内存),再创建vm_area_struct结构,更新页表,返回虚拟地址。

vm_area_struct的结构体如下

1. vm_start, vm_end表示区域的开始位置和结束位置,确定了区域的边界。两个vm_area_struct不会出现交叉的情况

2. vm_page_prot 表示这个区域的页的访问权限

3. shared结构处理有后备文件的内存映射,和后备文件的address_space地址空间关联起来

4. anon_vma_node, anon_vma处理匿名文件共享内存映射的情况,映射到同一物理内存页的映射都保存在一个链表中

5. vm_pgoff, vm_file都是处理有后备文件内存映射的情况,获得该映射在文件的页偏移量,以及打开文件file实例的信息

对于有后备文件的映射,内核还提供了一个优先查找树结构,来加速确定一个文件和所有映射到这个文件的虚拟内存区域vm_area_struct实例的关系,从而可以得到所有映射到这个文件的进程信息。这张图表示了一个后备文件被mmap映射后内核建立的一些数据结构,涉及到了内存管理的数据和文件系统的数据

内核提供了一系列的函数对虚拟内存区域vm_area_struct进行操作,比如创建,删除,合并,查找等等。而mmap是C标准库提供给用户程序的一个函数来使用内存映射,建立起文件地址空间和虚拟内存区域的映射关系。

mmap的4种类型

mmap分为有后备文件的映射和匿名文件的映射,这两种映射又有私有映射和共享映射之分,所以mmap可以创建4种类型的映射

1. 后备文件的共享映射,多个进程的vm_area_struct指向同一个物理内存区域,一个进程对文件内容的修改,会被其他进程可见。对文件内容的修改会被写回到后备文件。

2. 后备文件的私有映射,多个进程的vm_area_struct指向同一个物理内存区域,采用写时拷贝的方式,当一个进程对文件内容做修改,不会被其他进程看到。另外对文件内的修改也不会被写回到后备文件。当内存不够需要进行页回收时,私有映射的页被交换到交换区。一般用在加载共享代码库

3. 匿名文件的共享映射,内核创建一个初始都是0的物理内存区域,然后多个进程的vm_area_struct指向这个共享的物理内存区域,对该区域内容的修改对所有进程可见。匿名文件在页回收时被交换到交换区

4. 匿名文件的私有映射,内核创建一个初始都是0的物理内存区域,对该区域内容的修改只对创建者进程可见。匿名文件在页回收时被交换到交换区。malloc()底层是用了匿名文件的私有映射来分配大块内存。

比如下面的例子,mmap会涉及到物理内存的变化(加载后备文件到页缓存,或者分配都是0的物理内存块),创建vm_area_struct虚拟内存区域实例,更新页表

  1. // 后备文件的共享映射
  2. fd = open("/home/xxx/a.txt", O_RDWR)
  3. addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARD, fd, 0)
  4. // 匿名文件的私有映射
  5. fd = open("/dev/zero", O_RDWR)
  6. addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)

内存映射的用途很多,比如

1. 后备文件的共享映射可以用作内存映射IO来对大文件进行操作,比普通IO减少一次复制。需要注意的是内存映射IO涉及到内核的很多操作,比如vm_area_struct的创建,页表的修改等等,比普通IO的操作更复杂。小文件的读写使用普通IO更合适

2. 后备文件的私有映射可以用作共享库二进制文件代码段,数据段的加载

3. 匿名文件的共享映射可以用作fork时让父子进程共享匿名映射分配的内存

4. 匿名文件的私有映射可以用作进程的私有内存分配

内核对堆空间的管理

实际上从内核的管理用户进程虚拟地址空间的角度来说,内存映射是管理用户进程虚拟地址空间的主要手段,通过建立vm_area_struct来分配虚拟内存区域。内核对堆空间的分配主要是brk系统调用,brk系统调用本质上也是利用了匿名文件私有映射,分配初始化0的物理内存页,建立vm_area_struct,然后更新页表结构。brk系统调用分配的内存最小单位是页,需要按页对齐,从start_brk位置向上扩展,也就是说从内核的角度来说,每次对堆空间的分配最小就是一页,更细粒度的字节内存空间分配由C语言标准库实现的。

计算机底层知识拾遗(九)深入理解内存映射mmap的更多相关文章

  1. 深入理解内存映射mmap

    内存映射mmap是Linux内核的一个重要机制,它和虚拟内存管理以及文件IO都有直接的关系,这篇细说一下mmap的一些要点. 修改(2015-11-12):Linux的虚拟内存管理是基于mmap来实现 ...

  2. 内存映射mmap的几个api及其使用

    内存映射 mmap 内存映射mmap函数的作用是建立一段可以被两个或者多个程度读写的内存段,一个程序对他进行任何修改,对其它程序可见.同样,这个功能可以用在对文件的处理上,mmap函数创建一个指向一个 ...

  3. [转载]linux内存映射mmap原理分析【转】

    转自:http://www.cnblogs.com/wanpengcoder/articles/5306688.html 转自:http://blog.csdn.net/yusiguyuan/arti ...

  4. 内存映射MMAP和DMA【转】

    转自:http://blog.csdn.net/zhoudengqing/article/details/41654293 版权声明:本文为博主原创文章,未经博主允许不得转载. 这一章介绍Linux内 ...

  5. 内存映射mmap

    Table of Contents 1. 什么是mmap 2. 使用方法 2.1. mmap构造器的格式 2.2. 例子1 2.3. 例子2 3. 其它 4. 参考资料 什么是mmap 通常在Unix ...

  6. Linux内存映射(mmap)系列(1)

    看到同事的代码中出现了mmap.所以自己私下学习学习,研究研究..... http://www.cnblogs.com/lknlfy/archive/2012/04/27/2473804.html ( ...

  7. Linux内存映射--mmap函数

    Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改, 先来看一下mmap的函数声明: 头文件: < ...

  8. c++11-17 模板核心知识(九)—— 理解decltype与decltype(auto)

    decltype介绍 为什么需要decltype decltype(auto) 注意(entity) 与模板参数推导和auto推导一样,decltype的结果大多数情况下是正常的,但是也有少部分情况是 ...

  9. JAVA NIO之浅谈内存映射文件原理与DirectMemory

    JAVA类库中的NIO包相对于IO 包来说有一个新功能是内存映射文件,日常编程中并不是经常用到,但是在处理大文件时是比较理想的提高效率的手段.本文我主要想结合操作系统中(OS)相关方面的知识介绍一下原 ...

随机推荐

  1. MyBatis关联查询,一对一关联查询

    数据库E-R关系 实体类 public class City { Long id; String name; Long countryId; Date lastUpdate; } public cla ...

  2. CF1073E Segment Sum

    数位DP,求[L,R]区间内所有"数字内包含的不同数码不超过k个的数字"之和.在状态上加一维状态压缩表示含有的数码集合.一开始读错题以为是求数字的个数.读对题之后调了一会儿. #i ...

  3. 【bzoj5073】[Lydsy1710月赛]小A的咒语 后缀数组+倍增RMQ+贪心+dp

    题目描述 给出 $A$ 串和 $B$ 串,从 $A$ 串中选出至多 $x$ 个互不重合的段,使得它们按照原顺序拼接后能够得到 $B$ 串.求是否可行.多组数据. $T\le 10$ ,$|A|,|B| ...

  4. java 23种设计模式 深入浅出

    以下内容只作为对自己对知识进行总结,如有引用他人文章会在文段末尾表明出处: Java的23种设计模式 23种设计模式总共可以分为三大类,进行不定期更新总结,将逐步展开介绍自己对设计模式的理解,多多指教 ...

  5. BZOJ1018 SHOI2008堵塞的交通(线段树)

    动态图的连通性当然是可以用LCT维护的.但这相当的不优美,毕竟这样做没有用到任何该图的性质,LCT自带的大常数也会使其跑得非常慢. 考虑用线段树维护区间左右端四个点之间各自的连通性(仅经过该区间内路径 ...

  6. C++中 0 与 NULL 与 nullptr之间的关系,nullptr_t 的实现

    C++中 0 与 NULL 与 nullptr之间的关系,nullptr_t 的实现 来源 http://blog.csdn.net/Virtual_Func/article/details/4975 ...

  7. 利用powerful number求积性函数前缀和

    好久没更博客了,先水一篇再说.其实这个做法应该算是杜教筛的一个拓展. powerful number的定义是每个质因子次数都 $\geq 2$ 的数.首先,$\leq n$ 的powerful num ...

  8. 【转】树莓派Raspberry Pi - 还原已经装过系统的TF卡

    想给树莓派换个系统的话,需要先把已经装过系统的TF卡进行还原,这里使用最简单粗暴无脑的方法: 1,下载安装Win32 Disk Imager(一般已经装过一次系统后,这个东西都有) 2,下载boots ...

  9. 洛谷 P1654 OSU! 解题报告

    P1654 OSU! 题目描述 osu 是一款群众喜闻乐见的休闲软件. 我们可以把osu的规则简化与改编成以下的样子: 一共有\(n\)次操作,每次操作只有成功与失败之分,成功对应\(1\),失败对应 ...

  10. Java中如何遍历Map对象

    方法一:使用map.entrySet()来遍历.这是最常见的并且在大多数情况下也是最可取的遍历方式.在键值都需要的时候使用. Map<String,String> map = new Ha ...