【Linux系统编程】对mmap的理解

之前第一遍学习mmap的时候很多人都说它有很多优点,也翻了一些大佬的博客,发现并不是很能理解,最近钻研了虚拟内存的知识之后豁然开朗,让我对mmap有了新的认识,在这里简单的总结一下。
建议如果大家对一些概念性的东西不是很理解的话可以先看一看《深入理解计算机系统》的第九章,之后你可能会跟我一样豁然开朗哈哈哈。

Linux进程虚拟内存

【Linux系统编程】对mmap的理解
学过linux应该对这个图非常了解,从这个图中可以看出Linux把虚拟内存组织成一些段的集合,一个段就是已经分配的虚拟内存的连续片。
内核为每个进程维护了一个任务结构(task_struct),内核用这个结构体来描述一个进程,这个结构包含了进程的一些信息,包括进程id,可执行文件名,指向用户栈的指针以及上下文的task结构体的指针等等,不过其中我们最关心的还是其中指向mm_struct结构体的条目,这个结构体描述了当前进程的状态,而这个mm_struct结构体中有两个比较重要的条目,一个是pgd,他指向一级页表的基址,便于根据虚拟地址查询页表,另一个是mmap,他指向一个vm_area_struct的链式结构,这个链式结构用来描述一个段,其中包括一个段的起止虚拟地址,标志访问权限的权限位,标志是否是共享区的标志位。这些标志的意义就是CPU要操作一块数据时,内核会先遍历这个vm_area_struct链表(为了便于搜索,其实他既是一个链表,还是一个树),如果查不到属于某一块,就说明操作的地址还未分配,属于非法,内核会报段错误,并结束这个进程。当然如果权限不够也会报错。
【Linux系统编程】对mmap的理解

mmap实现原理

根据我们上面提到的vm_area_struct结构体,其实mmap的最主要的作用就是构建一个vm_area_struct节点,并把它插入到原来这个链表中。大概过程如下:

  1. 进程调用mmap函数
  2. 内核会遍历vm_area_struct链表,寻找一块未分配的虚拟内存地址段
  3. 为此段初始化vm_area_struct结构体,并把它插入到链表(树)中
  4. 从进程的已打开文件文件描述符集中定位到要映射的文件
  5. 调用更底层的系统mmap函数
  6. 根据其中维持的文件的各种信息定位到磁盘地址
  7. 根据磁盘地址更新页表信息,将其标志成已分配但未缓存
  8. 在需要对这片区域进行操作的时候引发缺页异常
  9. 判断物理页是否有空闲,如果有取一个新页来映射磁盘内容
  10. 如果没有空闲页,选好牺牲页 ,如果牺牲页是脏页写回磁盘,更新原进程页表,映射磁盘内容到这个牺牲页进行覆盖,更新当前进程的页表。
  11. 内核重复一次查询页表的操作,将虚拟地址翻译成物理地址,取数据,进行读写,如果写了标记脏页面

函数原型

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

  • 如果addr参数为NULL,内核会自己在进程地址空间中选择合适的地址建立映射。如果
    addr不是NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择addr之上的某个
    合适的地址开始映射。
  • length参数是需要映射的那一部分文件的长度。
  • prot
    • PROT_EXEC表示映射的这一段可执行,例如映射共享库
    • PROT_READ表示映射的这一段可读
    • PROT_WRITE表示映射的这一段可写
    • PROT_NONE表示映射的这一段不可访问
  • flag参数有很多种取值,这里只讲三种
    • MAP_SHARED多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修
      改,另一个进程也会看到这种变化。
    • MAP_PRIVATE多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修
      改,另一个进程并不会看到这种变化,也不会真的写到文件中去。
    • MAP_ANON匿名映射,不需要指定文件描述符。
  • 如果mmap成功则返回映射首地址,如果出错则返回常数MAP_FAILED。当进程终止时,该进程
    的映射内存会自动解除,也可以调用munmap解除映射。munmap成功返回0,出错返回-1。

mmap优点

  1. 对于普通io操作,内核会先把文件的内容缓存在内核的io缓冲区,当进程使用read时内核会把io缓冲区的内容在用户区复制一遍,这一步操作需要陷入内核态操作,同时在内核区和用户区都进行了缓存,操作起来比较麻烦,而且占用空间,但是mmap会直接跳过io缓冲区,直接在用户缓冲区进行读写,内核会在一段时间之后将用户缓冲区的内容写入磁盘。
  2. 通过这种方式可以实现进程间通信,多个进程通过映射到同一块内存物理地址来共享同段数据,以此来实现进程间进行交流。而且这种方式和pipe以及FIFO相比都快很多。
  3. 通过共享内存的方式可以扩展内存空间,比如malloc,其底层就是使用mmap匿名映射来开辟的空间。

这篇文章主要参考《深入理解计算机系统》以及其他大佬的博客进行编写的,如果有什么错误欢迎指出。