当前位置: 首页 > news >正文

【Linux】mmap的介绍和使用

参考文章:https://cloud.tencent.com/developer/article/2420465

一、mmap介绍

  • mmap (Memory Map) 是一种将文件或设备映射到内存的系统调用,允许应用程序直接通过内存地址访问文件数据,无需使用 read/write 等系统调用

  • 虽然 mmap() 最初是为映射文件而设计的,但它实际上是一个通用映射工具。它可用于将任何适当的对象(例如内存、文件、设备等)映射到进程的地址空间。

  • 以文件映射到内存为例,实现这样的映射后,进程虚拟地址空间中一段内存地址将与文件磁盘地址一一对应,进程就可以采用指针的方式读写这段内存,系统会自动回写脏 页到对应的磁盘文件。

在这里插入图片描述

二、mmap原理

2.1 mmap映射过程

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

  1. 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

    • 进程在用户空间调用函数 mmap
    • 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续虚拟地址
    • 为此虚拟区分配一个 vm_area_struct 结构,接着对这个结构的各个域进行初始化
    • 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
  2. 调用内核空间的系统调用函数 mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的映射

    • 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
    • 通过该文件的文件结构体,链接到file_operations 模块,调用内核函数 mmap,其原型为int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
    • 内核 mmap 函数通过虚拟文件系统 inode 模块定位到文件磁盘物理地址。
    • 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中
  3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存的拷贝

    • 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址对应的物理内存页面上没有数据。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存,因此引发缺页异常。
    • 缺页异常进行一系列判断,确定无非法操作后,内核发起调页过程。
    • 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用 nopage 函数把所缺的页从磁盘载入主存。
    • 之后进程即可对这片主存进行读写,如果写操作改变了其内容,一定时间后系统会自动回写脏页到对应磁盘地址,即完成了写入到文件的过程。
      在这里插入图片描述

2.2 mmap对比常规文件操作

常规文件操作,这里指的是read/write操作,通常有以下的执行过程,以read为例

  1. 进程发起读文件请求
  2. 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的 inode
  3. inodeaddress_space 上查找要请求的文件页是否已经缓存在页缓存。如果存在,则直接返回这片文件页的内容
  4. 如果不存在,则通过 inode 定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页请求,进而将页缓存中的数据发给用户进程

总的来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制:

  • 读文件时需要先将文件页从磁盘拷贝到页缓存。
  • 由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到用户空间内存
  • 这样,通过了两次数据拷贝,才能完成进程对文件内容的获取任务

写操作也是一样:

  • 待写入的 buffer 在内核空间不能直接访问,必须要先拷贝至内核空间内存,再写回磁盘中(延迟写回),也需要两次数据拷贝。

而使用 mmap 操作文件:

  • 首先需要创建新的虚拟内存区域
  • 然后建立文件磁盘地址和虚拟内存区域映射
  • 这两步没有任何文件拷贝操作,而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
系统调用
建立页表映射
访问触发缺页中断
加载文件数据
磁盘文件
应用程序
内核
虚拟内存
物理内存
页缓存 Page Cache
硬盘

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而 mmap 操作文件,只需要从磁盘到用户主存的一次数据拷贝,效率更高。

对比项传统写入(write内存映射写入(mmap
数据拷贝次数两次(用户空间 → 内核空间 → 磁盘)一次(内核空间 → 磁盘)
系统调用开销每次写入都需调用write,开销较高仅需mmapmunmap,写入时无系统调用
写入方式通过write等函数传递数据直接操作内存地址(如指针赋值、memcpy
内存占用依赖内核缓冲区大小映射区域占用虚拟内存(实际物理内存按需分配)
文件大小限制无明确限制(受文件系统和磁盘容量限制)需预先通过ftruncate设置文件大小
同步控制通过fsync强制同步通过msync强制同步
适用场景随机写入、小数据块写入、动态调整文件大小大数据块连续写入、频繁访问同一区域、文件与内存结构映射

性能对比

  • mmap 更高效
    • 大量连续写入(如文件复制、数据库批量写入)
    • 频繁随机访问(如内存数据库、索引文件)
    • 需与内存结构直接映射(如共享内存、进程间通信)
  • 传统写入更高效
    • 小数据块随机写入(如日志追加)
    • 文件大小动态变化(mmap需重新映射)

三、mmap应用

3.1 mmap常用API

常用的api是下面的三个,下面我们一一介绍

#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);
int msync(void *addr, size_t length, int flags);

mmap 函数原型

#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1. addr:映射起始地址
  • 作用:指定映射区域的起始地址。通常设为 NULL,让系统自动选择合适的地址。
  • 注意
    • 若不为 NULL,系统会尝试将映射区域起始于此地址,但最终地址可能因对齐要求而调整。
    • 大多数场景建议使用 NULL,避免兼容性问题。
2. length:映射区域大小(字节)
  • 作用:指定要映射的文件或设备的长度。
  • 要求
    • 必须为正整数。
    • 若文件长度小于 length,则映射区域超出文件部分的值为 0(称为 “文件空洞”)。
    • 若文件长度大于 length,则仅映射前 length 字节。
3. prot:内存保护标志(访问权限)
  • 可选值(按位或组合)
    • PROT_READ:可读
    • PROT_WRITE:可写
    • PROT_EXEC:可执行
    • PROT_NONE:不可访问
  • 限制
    • 权限不能超过文件打开模式(如 open 以只读模式打开,则 prot 不能包含 PROT_WRITE)。
    • 示例:PROT_READ | PROT_WRITE 表示可读可写。
4. flags:映射类型和行为标志
  • 必选标志
    • MAP_SHARED:创建共享映射,对映射区域的修改会反映到文件,并被其他映射同一文件的进程看到。
    • MAP_PRIVATE:创建私有映射,对映射区域的修改仅对当前进程可见,不影响原文件(写时复制机制)。
  • 可选标志(常用)
    • MAP_ANONYMOUS:映射匿名内存(不关联文件),此时 fd 需设为 -1offset 需为 0
    • MAP_FIXED:强制使用 addr 指定的地址(不推荐,可能导致地址冲突)。
    • MAP_NORESERVE:不预分配物理内存(适用于大映射,仅在实际访问时分配)。
5. fd:文件描述符
  • 作用:指定要映射的文件或设备。
  • 要求
    • 需通过 openshm_open 等函数预先打开。
    • 若使用 MAP_ANONYMOUS,则 fd 必须为 -1
6. offset:文件偏移量
  • 作用:指定从文件的哪个位置开始映射(以字节为单位)。
  • 要求
    • 必须是系统页大小(通常为 4KB)的整数倍。
    • 示例:offset = 4096 表示从文件第 4096 字节处开始映射。
返回值
  • 成功:返回映射区域的起始地址(类型需转换为 char* 或其他指针类型)。
  • 失败:返回 MAP_FAILED(即 (void*)-1),并设置 errno 以指示错误类型。
常见错误码
  • EACCES:权限不足(如文件只读但 prot 包含 PROT_WRITE)。
  • EINVAL:参数无效(如 offset 不是页大小的整数倍)。
  • ENOMEM:内存不足或地址空间不足。
  • EBADFfd 不是有效的文件描述符。

munmap函数原型

#include <sys/mman.h>int munmap(void *addr, size_t length);
1. addr:映射区域起始地址
  • 作用:指定要解除映射的内存区域的起始地址,必须与 mmap 返回的地址完全一致。
  • 注意
    • 若传入非法地址(如未映射的地址或部分映射的地址),会导致 EINVAL 错误。
    • 即使映射区域已被部分释放,也必须传入完整的起始地址。
2. length:映射区域长度
  • 作用:指定要解除映射的内存区域的长度(字节),必须与 mmap 时指定的长度一致。
  • 注意
    • length 与原映射长度不一致,可能导致未定义行为。
    • 必须解除整个映射区域,不支持部分解除。
返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置 errno 以指示错误类型。
常见错误码
  • EINVAL
    • addr 不是有效的映射起始地址。
    • length0
  • ENOMEM
    • 内核内部内存不足,无法完成操作

msync函数原型

#include <sys/mman.h>int msync(void *addr, size_t length, int flags);
1. addr:映射区域起始地址
  • 作用:指定要同步的内存映射区域的起始地址,必须与 mmap 返回的地址一致。
  • 注意
    • 若传入非法地址,会导致 EINVAL 错误。
    • 地址需按页对齐(通常为 4KB 边界),否则行为未定义。
2. length:同步区域长度
  • 作用:指定从 addr 开始的连续字节数,需同步的区域为 [addr, addr+length)
  • 特殊情况
    • length 为 0,则不执行同步操作,但函数仍可能返回成功(0)。
    • length 超过映射区域边界,会导致 EINVAL 错误。
3. flags:同步行为标志
  • 必选标志(三选一)
    • MS_SYNC:同步模式,阻塞直到所有数据写入磁盘。
      • 保证:调用返回成功时,数据已持久化到磁盘(即使系统崩溃也不会丢失)。
      • 性能影响:可能导致明显延迟,尤其在机械硬盘上。
    • MS_ASYNC:异步模式,立即返回,内核异步将数据写入磁盘。
      • 保证:仅标记数据需要写入,不等待实际磁盘操作完成。
      • 适用场景:性能敏感场景,可容忍一定数据丢失风险。
    • MS_INVALIDATE:使缓存失效,强制重新从磁盘读取数据。
      • 作用:用于刷新其他进程对文件的修改,确保后续读取到最新内容。
      • 注意:与 MS_SYNC/MS_ASYNC 互斥,不可同时使用。
  • 可选标志(与上述标志组合)
    • MS_KERNEL:Linux 特有的标志,指示同步内核缓存(而非用户空间缓存)。
      • 通常无需显式指定,默认行为已包含此逻辑。
返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置 errno 以指示错误类型。
常见错误码
  • EINVAL
    • flags 包含无效组合(如同时指定 MS_SYNCMS_INVALIDATE)。
    • addrlength 无效(如非页对齐地址)。
  • EBUSY
    • 映射区域正被内核锁定,无法立即同步 。
  • EIO
    • 磁盘 I/O 错误,可能导致数据未完全写入。

3.2 mmap读取文件

首先,我们准备一个文本文件,生成一些内容,用于测试

int generateTest()
{int fd = open("1.txt", O_CREAT | O_WRONLY);if (fd == -1){perror("open");return 1;}for (int i = 1; i <= 1000000; ++i){std::string info = "This is a mmap test" + std::to_string(i) + "\n";write(fd, info.c_str(), info.size());}close(fd);return 0;
}

使用mmap进行文件读操作

int test1()
{int fd = open("1.txt", O_RDONLY);struct stat sb;if (fstat(fd, &sb) == -1){perror("fstat");return 1;}char *addr = (char *)mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);if (addr == MAP_FAILED){perror("mmap");return 1;}std::cout << "===== File Content =====\r\n";std::cout << addr;if (munmap(addr, sb.st_size)){perror("munmap");return 1;}close(fd);return 0;
}

为了对比mmap的效率,我们这里新增使用read直接读取数据的例子

int test2()
{int fd = open("1.txt", O_RDONLY);struct stat sb;if (fstat(fd, &sb) == -1){perror("fstat");return 1;}std::cout << "===== File Content =====\r\n";char buffer[1024];int len = 0;while ((len = read(fd, buffer, sizeof(buffer) - 1)) != 0){buffer[len] = '\0';std::cout << buffer;}close(fd);return 0;
}

如果只是小文件,mmap体现不了效率,因为建立映射也需要额外的开销

在这里插入图片描述

如果是大文件读取,这里的效率就可以看出来了,mmap比直接read快了将近1000ms

在这里插入图片描述

3.3 mmap写文件

mmap写文件,首先需要使用ftruncate提前指定文件的大小,注意这个函数只是提前将文件(通过 fd 指定)的大小调整为 length 字节,而不会实际分配内存

函数原型如下:

#include <unistd.h>int ftruncate(int fd, off_t length);
  • 调整文件大小后,就可以使用mmap建立映射关系了,得到的内存可以像普通内存一样操作
  • 这里通过循环的memcpy将写入的信息拷贝到虚拟内存中,由内核将缓存写入磁盘

写入虚拟内存后,还可以使用msync同步到磁盘,这里指定策略为MS_SYNC,一直阻塞到内存刷入磁盘,如果不调用个函数,也会在特定情况下内核自动刷新到磁盘中

最后调用munmap取消内存映射,释放虚拟内存

#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<iostream>
#include<cstring>
#include<unistd.h>#define WRITE_LINES 100000
std::string strBuffer[WRITE_LINES + 1];int main(){int fd = open("2.txt",O_RDWR| O_CREAT,0644);if(fd == -1){perror("open");return 1;}int length = 0;for(int i = 1 ; i <= WRITE_LINES ; ++i){std::string info = "Hello mmap:" + std::to_string(i) + "\n";strBuffer[i] = info;length += info.size();}if(ftruncate(fd,length) == -1){perror("ftruncate");return 1;}char* addr = (char*)mmap(NULL,length,PROT_READ | PROT_WRITE, MAP_SHARED,fd,0);if(addr == MAP_FAILED){perror("mmap");return 1;}int j = 0;for(int i = 1 ; i <= WRITE_LINES ; ++i){memcpy(addr + j,strBuffer[i].c_str(),strBuffer[i].size());j += strBuffer[i].size();}if(msync(addr,length,MS_SYNC) == -1){perror("msync");return 1;}munmap(addr,length);close(fd);return 0;
}

运行程序后,查看写入的文件,可以查看到对应的内容

cat 2.txt

在这里插入图片描述

3.4 mmap进程间通讯

mmap 也可以配合 MAP_ANONYMOUS 标志,实现父子进程间的内存共享通信

主要的步骤如下:

  1. 创建一块匿名共享内存区域
  2. 通过 fork() 创建子进程
  3. 子进程向共享内存写入消息
  4. 父进程等待子进程结束后,从共享内存读取消息
  5. 释放共享内存资源

注意,为了实现父子进程之间的通讯,mmap函数中必须使用MAP_ANONYMOUS标志,创建匿名映射(不关联文件),以及MAP_SHARED允许多进程共享该内存区域

关联后得到的内存块,就可以像system V共享内存那样使用了

#include<iostream>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
#include<cstring>int main(){ssize_t size = 4096;char * shared = (char*)mmap(NULL,size,PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS,-1,0);if(shared == MAP_FAILED){perror("mmap");return 1;}pid_t pid = fork();if(pid == -1){perror("fork");return 1;}if(pid == 0){ std::string info = "Hello from child process !";memcpy(shared,info.c_str(),info.length());return 0;}else{wait(NULL);std::cout << "Receive from child process : " << shared << std::endl;}munmap(shared,size);return 0;
}

运行结果如下:

在这里插入图片描述

更多资料:https://github.com/0voice

http://www.dtcms.com/a/288161.html

相关文章:

  • [硬件电路-36]:模拟电路的基本组成要素以及模拟信号处理
  • Python条件控制艺术:侦探破解犯罪谜题逻辑
  • 浏览器渲染原理——计算属性和布局过程常考内容
  • 如何实现一个定时任务
  • LibreTv在线观影项目部署开箱即用
  • 如何解决Flink CDC同步时间类型字段8小时时间差的问题,以MySQL为例
  • 相似度度量方法
  • 车载刷写框架 --- 关于私有节点刷写失败未报引起的反思
  • 暑期算法训练.4
  • 用虚拟机体验纯血鸿蒙所有机型!
  • 【成品设计】基于STM32的水资源监控系列项目
  • 几个好用的MCP分享
  • 使用 PlanetScope 卫星图像绘制水质参数:以莫干湖为例
  • 创建第二大脑--第五章 组织:以行动为导向
  • 使用Python进行文件拷贝的方法
  • NLP中情感分析如何结合知识图谱在跨文化领域提升观念分析和价值判断的准确性?
  • Dockerfile格式
  • windows wsl ubuntu 如何安装 open-jdk8
  • [硬件电路-39]:激光光路的光信号处理、模拟电路的电信号处理、数字电路的电信号处理、软件的信号处理,有哪些共通的操作、运算、变换?
  • BabyAGI 是一个用于自构建自主代理的实验框架
  • Java脚本API参数传递机制详解
  • 让Logo/文字“自己画自己”!✨
  • 一套完整的反向海淘代购系统是一项复杂的系统工程,需要整合电商、物流、支付、清关、仓储、用户服务等多个环节
  • Codeforces Round 1037(Div.3)
  • C++ 比较器(Comparator)超详细笔记
  • 轻松学习C++:基本语法解析
  • JAVA高级第六章 输入和输出处理(一)
  • Git仓库使用
  • MacOS:如何利用终端来操作用户
  • 品鉴笔记:智利美人鱼磨坊甜红与甜白的风味对比