1. mmap介绍

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享

2. Linux内存管理介绍

结构体定义

在Linux操作系统中,一切皆文件,所有概念都被通过文件描述符进行抽象建模来进行标识,内存管理也是如此,如下是Linux中进程、内存管理的结构描述。

进程结构体(task_struct)

task_struct结构体中,进程地址空间通过以下成员变量指向mm_struct即内存空间,每个进程空间都有自身的内存空间。

相关视频推荐

【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!~点击832218493加入(需要自取)

内存结构体(mm_struct)

mm_struct内存结构体中,通过两种数据结构来表示vm_area_struct内存空间,当内存空间较少时采用链表进行管理,当内存空间较多时会采用红黑树进行管理。下面是mm_struct的类结构,内容较多,为了保证完整性没有做删减聚焦。

虚拟内存区域结构体(vm_area_struct)

vm_area_struct可以说是虚拟内存的最小结构单元,主要通过vm_start、vm_end表示内存空间的起止位置,通过vm_prot表示内存区域保护方式,通过vm_next表示下一块内存空间引用,通过vm_flags表示内存空间区域的特性,等等。Linux提供的mmap函数方法会影响它们。

页表管理

Linux Kernel 使用内存管理的时候,采取的是页式的管理方式,应用程序给出的内存地址是虚拟地址,是经过若干层的页表的转换才能得到真正的物理地址,所以相对来说,进程的地址空间是一份虚拟的地址空间,每一个地址通过页表的转换映射到所谓的物理地址空间上。

3. mmap内存映射剖析

内存&文件映射结构

如上介绍Linux内存管理方式,进程、进程内存、mmap的关系图如上

  • 进程(Process) 通过task_struct进行表示,会包含一个内存结构体mm_struct
  • 内存(Memory) 通过mm_struct进行标识,会包含一个内存映射体mmap栈、堆、bss数据段、初始化数据段、text数据段等内存空间组成,这些组成体都是通过vm_area_struct这个最小的内存管理组成单元进行构建和表示的
  • 内存空间区域(Virtual Memory Area) 内存空间区域即组成内存管理的最小单元,里面包含vm_start、vm_end、vm_next等变量进行内存页结构的表示与维护

内存&文件映射过程

mmap内存映射的实现过程,总的来说可以分为三个阶段:

  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
    • 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
    • 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
    • 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
    • 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
  • 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
    • 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
    • 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
    • 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
    • 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
    • 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
    • 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
    • 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
    • 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
值得注意的是:
  • 前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时
  • 修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用 msync() 来强制同步, 这样所写的内容就能立即保存到文件里了。


4. mmap函数

语法

void mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);

功能

  • 将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,以获得较高的性能
  • 将特殊文件进行匿名内存映射,为关联进程提供共享内存空间
  • 为无关联的进程间的Posix共享内存(SystemV的共享内存操作是shmget/shmat)

参数

  • 参数addr指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
  • 参数length代表将文件中多大的部分映射到内存。
  • 参数prot映射区域的保护方式。可以为以下几种方式的组合:

参数flags影响映射区域的各种特性,在调用mmap()时必须要指定。


  • 参数fd要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1
  • 参数offset文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍

mmap函数只是完成建立mmap虚拟内存空间与文件的地址映射,并没有进行数据拷贝

mumap函数

解除虚拟内存与文件映射关系

mprotect函数

对内存空间数据进行保护设置

msync函数

把映射区域的修改回写到后备存储中.因为munmap时并不保证页面回写,如果不调用msync,那么有可能在munmap后丢失对映射区的修改.其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后立即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射.该系统调用是通过调用映射文件的sync函数来完成工作的.

brk函数

将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分相似,同样是产生一个vma,然后指定其属性.不过在此之前需要做一些合法性检查,比如该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等.通过brk产生的vma映射的文件为空,这和匿名映射产生的vma相似,关于匿名映射不做进一步介绍.库函数malloc就是通过brk实现的

5. mmap交互过程跟踪

在JDK中,DirectByteBuffer 就是利用mmap机制实现的IO高性能操作。下面先简单编写一个实现DEMO如下:

编译DEMO后放置在Linux系统上执行,通过strace命令来查看服务进程与操作系统之间的交互情况

strace -o log.txt -T -tt -e trace=memory java com/github/java/learning/nio/ByteBufferTest

运行后log.txt日志输出的交互情况如下:

15:47:00.969923 brk(NULL) = 0x4010a000 <0.000009> 15:47:00.970083 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8eab000 <0.000007> 15:47:00.970374 mmap(NULL, 37494, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8ae8ea1000 <0.000008> 15:47:00.970456 mmap(NULL, 2208904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8ae8a6f000 <0.000009> 15:47:00.970499 mprotect(0x7f8ae8a86000, 2093056, PROT_NONE) = 0 <0.000010> 15:47:00.970530 mmap(0x7f8ae8c85000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16000) = 0x7f8ae8c85000 <0.000012> 15:47:00.970567 mmap(0x7f8ae8c87000, 13448, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8c87000 <0.000009> 15:47:00.970654 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8ea0000 <0.000008> 15:47:00.970687 mmap(NULL, 1089200, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8ae8d96000 <0.000008> 15:47:00.970713 mprotect(0x7f8ae8d9d000, 1052672, PROT_NONE) = 0 <0.000010> 15:47:00.970739 mmap(0x7f8ae8e9e000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8000) = 0x7f8ae8e9e000 <0.000009> 15:47:00.970833 mmap(NULL, 2109744, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8ae886b000 <0.000010> 15:47:00.970862 mprotect(0x7f8ae886d000, 2097152, PROT_NONE) = 0 <0.000011> 15:47:00.970889 mmap(0x7f8ae8a6d000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f8ae8a6d000 <0.000009> 15:47:00.970991 mmap(NULL, 3940800, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8ae84a8000 <0.000008> 15:47:00.971021 mprotect(0x7f8ae8660000, 2097152, PROT_NONE) = 0 <0.000016> 15:47:00.971054 mmap(0x7f8ae8860000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b8000) = 0x7f8ae8860000 <0.000011> 15:47:00.971088 mmap(0x7f8ae8866000, 16832, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8866000 <0.000008> 15:47:00.971145 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8d95000 <0.000007> 15:47:00.971182 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8d93000 <0.000007> 15:47:00.971301 mprotect(0x7f8ae8860000, 16384, PROT_READ) = 0 <0.000009> 15:47:00.971341 mprotect(0x7f8ae8a6d000, 4096, PROT_READ) = 0 <0.000008> 15:47:00.971383 mprotect(0x7f8ae8c85000, 4096, PROT_READ) = 0 <0.000009> 15:47:00.971416 mprotect(0x7f8ae8eac000, 4096, PROT_READ) = 0 <0.000008> 15:47:00.971441 munmap(0x7f8ae8ea1000, 37494) = 0 <0.000024> 15:47:00.971634 brk(NULL) = 0x4010a000 <0.000006> 15:47:00.971661 brk(0x4012b000) = 0x4012b000 <0.000007> 15:47:00.971683 brk(NULL) = 0x4012b000 <0.000005> 15:47:00.971830 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8ae8eaa000 <0.000009> 15:47:00.971905 munmap(0x7f8ae8eaa000, 4096) = 0 <0.000011> 15:47:00.972194 brk(NULL) = 0x4010a000 <0.000007> 15:47:00.972252 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4d1aa2e000 <0.000008> 15:47:00.972780 mmap(NULL, 37494, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4d1aa24000 <0.000008> 15:47:00.972863 mmap(NULL, 2208904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d1a5f2000 <0.000009> 15:47:00.972891 mprotect(0x7f4d1a609000, 2093056, PROT_NONE) = 0 <0.000010> 15:47:00.972917 mmap(0x7f4d1a808000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16000) = 0x7f4d1a808000 <0.000011> 15:47:00.972952 mmap(0x7f4d1a80a000, 13448, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f4d1a80a000 <0.000009> 15:47:00.973043 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4d1aa23000 <0.000008> 15:47:00.973079 mmap(NULL, 1089200, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d1a919000 <0.000007> 15:47:00.973105 mprotect(0x7f4d1a920000, 1052672, PROT_NONE) = 0 <0.000009> 15:47:00.973130 mmap(0x7f4d1aa21000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8000) = 0x7f4d1aa21000 <0.000009> 15:47:00.973252 mmap(NULL, 2109744, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d1a3ee000 <0.000008> 15:47:00.973281 mprotect(0x7f4d1a3f0000, 2097152, PROT_NONE) = 0 <0.000011> 15:47:00.973310 mmap(0x7f4d1a5f0000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f4d1a5f0000 <0.000009> 15:47:00.973435 mmap(NULL, 3940800, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d1a02b000 <0.000009> 15:47:00.973478 mprotect(0x7f4d1a1e3000, 2097152, PROT_NONE) = 0 <0.000013> 15:47:00.973510 mmap(0x7f4d1a3e3000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b8000) = 0x7f4d1a3e3000 <0.000011> 15:47:00.973543 mmap(0x7f4d1a3e9000, 16832, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f4d1a3e9000 <0.000008> 15:47:00.973599 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4d1a918000 <0.000008> 15:47:00.973635 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4d1a916000 <0.000007> 15:47:00.973743 mprotect(0x7f4d1a3e3000, 16384, PROT_READ) = 0 <0.000009> 15:47:00.973782 mprotect(0x7f4d1a5f0000, 4096, PROT_READ) = 0 <0.000008> 15:47:00.973821 mprotect(0x7f4d1a808000, 4096, PROT_READ) = 0 <0.000008> 15:47:00.973854 mprotect(0x7f4d1aa2f000, 4096, PROT_READ) = 0 <0.000008> 15:47:00.973879 munmap(0x7f4d1aa24000, 37494) = 0 <0.000012> 15:47:00.974056 brk(NULL) = 0x4010a000 <0.000006> 15:47:00.974083 brk(0x4012b000) = 0x4012b000 <0.000007> 15:47:00.974105 brk(NULL) = 0x4012b000 <0.000005> 15:47:00.974247 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4d1aa2d000 <0.000009> 15:47:00.974322 munmap(0x7f4d1aa2d000, 4096) = 0 <0.000012> 15:47:00.974496 mmap(NULL, 12625408, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d19420000 <0.000009> 15:47:00.974530 mprotect(0x7f4d19d3a000, 1056768, PROT_NONE) = 0 <0.000008> 15:47:00.974555 mmap(0x7f4d19e3c000, 1789952, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x91c000) = 0x7f4d19e3c000 <0.000010> 15:47:00.974589 mmap(0x7f4d19ff1000, 235008, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f4d19ff1000 <0.000010> 15:47:00.974704 mmap(NULL, 37494, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4d1aa24000 <0.000008> 15:47:00.974790 mmap(NULL, 3150136, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d1911e000 <0.000009> 15:47:00.974818 mprotect(0x7f4d1921f000, 2093056, PROT_NONE) = 0 <0.000011> 15:47:00.974845 mmap(0x7f4d1941e000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x100000) = 0x7f4d1941e000 <0.000010> 15:47:00.974964 mprotect(0x7f4d1941e000, 4096, PROT_READ) = 0 <0.000009> 15:47:00.976963 munmap(0x7f4d1aa24000, 37494) = 0 <0.000013> 15:47:00.977029 mmap(NULL, 1052672, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f4d1901d000 <0.000009> 15:47:00.977076 mprotect(0x7f4d1901d000, 4096, PROT_NONE) = 0 <0.000008>

梳理日志文本,可以看到主要调用的函数为mmap、mprotect、brk、mumap,下面通过man命令来查看函数的使用介绍梳理如下:

  • mmap 它把文件内容映射到一段内存上(准确说是虚拟内存上),通过对这段内存的读取和修改,实现对文件的读取和修改
  • mprotect 可以用来修改一段指定内存区域的保护属性
  • brk 改变数据段所用内存的数量
  • mumap 将文件内容与虚拟内存映射关系解除

下面通过ps -ef |grep java命令找到运行的java进程来查看

root 23866 23864 0 15:46 pts/1 00:00:00 java com/github/java/learning/nio/ByteBufferTest

此时进程ID是23866,通过命令 ll /proc/23866/fd/ ,查看文件描述符如下:

lrwx------ 1 root root 64 Jan 18 16:08 0 -> /dev/pts/1 lrwx------ 1 root root 64 Jan 18 16:08 1 -> /dev/pts/1 lrwx------ 1 root root 64 Jan 18 16:07 2 -> /dev/pts/1 lr-x------ 1 root root 64 Jan 18 16:08 3 -> /export/servers/jdk1.6.0_25/jre/lib/rt.jar

6. mmap与常规IO区别

常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

7. mmap作用及优点

  • 将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能
  • 将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间
  • 为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中