【Linux手册】mmap 接口:内存映射实现高效 IO 的

文章目录
- 前言
- 一. `mmap`接口的介绍与使用
- 二. 基于`mmap`简单实现`malloc/free`
- 三. `mmap`的优劣
- 3.1 优点
- 3.2 缺点
前言
在对文件进行读写的过程中,必须调用底层的接口read/write
,读取时需要的将数据从磁盘拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区中,写入时相反,需要将数据从用户缓冲区拷贝到内核缓冲区,再从内服缓冲区拷回磁盘。
通过以上的操作我们进行两次拷贝才能读写磁盘上的文件,上面有一步是不是可以省略,为什么要有内核缓冲区???
内核缓冲区本身也就在内存中,如果进程的虚拟地址直接与内存建立映射关系不就能对数据进行读取和写入嘛,通过这种方式就不需要再将数据从内核拷贝到用户缓冲区中,减少了一次拷贝。
操作系统中为我们提供了将文件直接映射到进程地址空间的接口:mmap
本文将分为3部分介绍mmap
:
mmap
接口的介绍与使用;- 基于
mmap
简单实现malloc/free
; mmap
的优劣。
一. mmap
接口的介绍与使用
将文件映射到进程的虚拟地址上:
void* mmap(void* addr , size_t length , int prot , int flags , int fd , off_t offste)
:
- 第一个参数,用来指定映射到虚拟地址的具体位置,使用
nullptr
可以让内核自动分配; - 第二个参数,长度,表示从文件中映射多大的内容到进程地址空间中;因为内存是以页划分的,所以该大小必须时4KB的整数倍,如果填的不是4KB的整数倍,操作系统也会将其转化为4KB的整数倍。
- 第三个参数,权限,表示是进行读
PROT_READ
,写PROT_WRITE
,可执行PROT_EXEC
,可以将这些选项组合使用; - 第四个参数,选项,常用的选项有两个:
MAP_PRIVATE
:创建私有映射,进程对虚拟地址空间内容修改不会反映到底层文件;MAP_SHARED
:建立共享映射,进程的修改会反映到底层文件上; - 第五个参数,要进行映射的文件描述符,也就是说在映射之前,我们就需要先将文件打开;
- 第六个参数,从文件的那个位置进行映射;
- 返回值:返回成功映射后的起始地址,失败会返回
MAP_FAILED
就是一个宏,将-1转化为void*。
映射的时候需要开辟使用进程的虚拟地址空间,因此使用往后要取消映射,来规范进程地址空间:
int munmap(void* addr , size_t length)
:
- 第一个参数:映射的起始地址;
- 第二个参数:映射的大小;
- 返回值:0表示成功,-1表示失败。
细节:在使用mmap
通过映射的方法向文件中进行写入的时候,文件大小并不会向write
一样自动增长,一次在建立映射之前,需要先保证文件足够大;
操作系统中提供了int ftruncate(int fd , size_t size)
接口来对文件进行扩容。
在进行读取文件内容的时候,同样也需要获取文件的大小,操作系统中提供了struct stat
和int stat(const char *restrict pathname , struct stat *restrict statbuf)
来获取文件的大小。
下面简单实现一个接口,将文件映射到内存地址空间上,通过对虚拟地址上的操作来修改文件内容:
int main()
{// 先打开一个文件int fd = open("./log.txt" , O_CREAT | O_RDWR | O_TRUNC , 0666);if(fd < 0){std::cerr << "open file error , in file :" << __FILE__ << " line : " << __LINE__ << std::endl;;exit(1);}// 修改文件大小if(ftruncate(fd , 1024) < 0){std::cerr << "ftruncate error , in file :" << __FILE__ << " line : " << __LINE__ << std::endl;exit(5);}// 进行文件映射void* p = mmap(nullptr , 1024 , PROT_WRITE | PROT_READ , MAP_SHARED , fd , 0);if(p == MAP_FAILED){std::cerr << "mmap error , in file :" << __FILE__ << " line : " << __LINE__ << std::endl;exit(2);}// 写入数据char* pch = static_cast<char*>(p);for(int i = 0 ; i < 26 ; i++)pch[i] = 'a' + i;// 关闭映射 , 并关闭文件if(munmap(p , 1024) < 0){std::cerr << "munmap error , in file :" << __FILE__ << " line : " << __LINE__ << std::endl;exit(3);}close(fd);return 0;
}
以上代码打开文件,并将文件进行扩容,先文件中写入26个英文字符。
二. 基于mmap
简单实现malloc/free
在上面mmap
的接口中我们可以使用MAP_PRIVATE
在进程地址空间上建立虚拟映射,虚拟映射并不会影响文件中的内容,进行是将文件中的数据映射到进程地址空间中,如果我们没有文件进行映射,不就是仅仅开辟一块虚拟地址进行使用嘛。
一次依据以上原理,我们就可以实现malloc/free
此时我们没有文件了,所以不能对文件进行扩容来获取大小,所以在选项中加入MAP_ANONYMOUS
表示:映射的内存区域不与任何磁盘文件关联
// 自制malloc
void* my_malloc(size_t size)
{if(size > 0){void* ptr = mmap(nullptr , size , PROT_WRITE | PROT_READ , MAP_PRIVATE | MAP_ANONYMOUS , -1 , 0);if(ptr == MAP_FAILED){return nullptr;}return ptr;}return nullptr;
}void my_free(void* ptr , size_t size)
{if(ptr!= nullptr && size > 0){if(munmap(ptr , size) < 0){std::cerr << "munmap error" << std::endl;}}
}
三. mmap
的优劣
3.1 优点
- 减少数据拷贝:仅保留磁盘与内核缓冲区之间的必要数据读取和写入,提升了数据传输效率,特别适合大文件读写场景;
- 高效的文件访问:将文件内容映射到进程虚拟地址空间后,进程可像访问内存一样直接读写文件,通过指针操作简化编程,减少了 I/O 系统调用次数,降低了系统开销。
- 进程间通信优势:通过
MAP_SHARED
模式,多个进程可共享同一块映射内存区域,实现低延迟的数据交换,是一种高效的进程间通信方式,类似于共享内存。 - 按需加载:基于页式映射的懒加载机制,仅在访问映射区域时才加载对应文件页到物理内存,避免一次性将大文件全部读入内存,节省内存资源;
- 内存映射设备访问:能将硬件设备的内存区域映射到进程地址空间,方便用户态程序直接访问硬件设备,减少系统调用开销。
3.2 缺点
- 固定映射开销:对于小文件,建立映射关系所涉及的系统操作(如分配虚拟地址区域、创建映射元数据等)产生的固定开销,可能会超过其带来的性能收益,导致使用
mmap
比传统read
/write
更慢; - 虚拟地址空间占用:映射会占用进程的虚拟地址空间,在 32 位系统中,由于虚拟地址空间有限,可能无法映射过大的文件;并且频繁使用
mmap
还可能导致虚拟地址空间碎片化,影响后续内存分配; - 同步控制问题:
mmap
写操作默认由操作系统异步同步到磁盘,在系统崩溃等异常情况下,可能会丢失未及时同步的数据; - 缺页中断延迟:当访问映射区域中尚未加载到物理内存的页时,会触发缺页中断,由内核从磁盘读取数据到内存。在随机访问映射区域的场景中,频繁的缺页中断可能导致性能下降;
- 共享映射安全风险:多进程共享映射区域时,一个进程的越界或错误操作可能会损坏其他进程的数据,缺乏传统 I/O 方式在用户缓冲区和内核缓冲区之间的隔离性,需要额外的同步和保护机制来确保数据安全;
- 不适合实时性要求高的场景:由于 I/O 操作的时机由内核调度,无法精确控制数据的读写时刻,所以
mmap
不太适用于对操作时序要求严格的实时性应用场景。