mmap就是文件映射内存的系统调用
一、传统IO方式
基于传统的IO方式,底层实际上通过调用read()
和write()
来实现。
- 把文件内容读入到内存中。
- 修改内存中的内容。
- 把内存的数据写入到文件中。
通过read()
把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()
写入到socket缓冲区
,最后写入网卡设备,过程如下图。
图1
从图 1 中可以看出,页缓存(page cache)
是读写文件时的中间层,内核使用 页缓存
与文件的数据块关联起来。所以应用程序读写文件时,实际操作的是 页缓存
。
1.那什么是页缓存呢?
文件一般都是存放在硬盘中,CPU并不能直接访问硬盘中的数据,而是需要先将硬盘中的数据读到内存中,然后才能被CPU访问。
由于读写硬盘的速度比读写内存要慢的多(DDR4内存读写速度是机械硬盘的500倍,是固态硬盘的200倍),所以为了避免每次读写文件时,都需要对硬盘进行读写操作,Linux内核使用页缓存(page cache)机制来对文件中的数据进行缓存。
图2
当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户。
当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。
2.读写时发生的切换和拷贝
图3
如上图2其整个过程发生了4次用户态和内核态的上下文切换和4次拷贝,具体流程如下:
- 用户进程通过
read()
方法向操作系统发起调用,此时上下文从用户态转向内核态 - DMA控制器把数据从硬盘中拷贝到读缓冲区
- CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,
read()
返回 - 用户进程通过
write()
方法发起调用,上下文从用户态转为内核态 - CPU将应用缓冲区中数据拷贝到socket缓冲区
- DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,
write()
返回
二、mmap
1.为什么要使用mmap
从传统读写文件的过程中,我们可以发现有个地方可以优化:如果可以直接在用户空间读写 页缓存
,那么就可以免去将 页缓存
的数据复制到用户空间缓冲区的过程。
那么,有没有这样的技术能实现上面所说的方式呢?那么 mmap就出现了。
使用 mmap
系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。原理如图 4所示:
图4
前面我们介绍过,读写文件都需要经过 页缓存
,所以 mmap
映射的正是文件的 页缓存
,而非磁盘中的文件本身。由于 mmap
映射的是文件的 页缓存
,所以就涉及到同步的问题,即 页缓存
会在什么时候把数据同步到磁盘。
Linux 内核并不会主动把 mmap
映射的 页缓存
同步到磁盘,而是需要用户主动触发。同步 mmap
映射的内存到磁盘有 4 个时机:
- 调用
msync
函数主动进行数据同步(主动)。 - 调用
munmap
函数对文件进行解除映射关系时(主动)。 - 进程退出时(被动)。
- 系统关机时(被动)。
2.mmap+write
mmap+write简单来说就是使用mmap
替换了read+write中的read操作,减少了一次CPU的拷贝。
mmap
主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。
图5
如图5整个过程发生了4次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
- 用户进程通过
mmap()
方法向操作系统发起调用,上下文从用户态转向内核态 - DMA控制器把数据从硬盘中拷贝到读缓冲区
- 上下文从内核态转为用户态,mmap调用返回
- 用户进程通过
write()
方法发起调用,上下文从用户态转为内核态 - CPU将读缓冲区中数据拷贝到socket缓冲区
- DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,
write()
返回
mmap
的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
三、总结
本文主要介绍了传统IO方式和 mmap
的原理和使用方式,在其中也顺带简述了page cache。
通过本文我们可以知道,使用 mmap
对文件进行读写操作时可以减少内存拷贝的次数,并且可以减少系统调用的次数,从而提高对读写文件操作的效率。
由于内核不会主动同步 mmap
所映射的内存区中的数据,所以在某些特殊的场景下可能会出现数据丢失的情况(如断电)。为了避免数据丢失,在使用 mmap
的时候可以在适当时主动调用 msync
函数来同步映射内存区的数据。
如果觉得有收获请点赞加关注,谢谢