mmap 内存映射:背景、原理
1. mmap 出现的背景在 mmap 出现之前进程读写文件主要依赖 read / write 这类传统 I/O 接口。以读取文件为例一次典型的 read 调用通常要经历两次数据拷贝磁盘数据通过 DMA 拷贝到内核页缓存内核把页缓存中的数据拷贝到用户态缓冲区(buf)然后应用程序再从用户态缓冲区buf读取数据这种方式存在几个明显问题多次用户态与内核态之间的数据拷贝CPU 开销较大read 每次只能处理用户指定的字节数处理大文件时需要反复调用系统调用开销高对于数据库、文件搜索、进程间共享数据等场景read / write 的抽象不够高效。为了提高文件访问性能并复用操作系统已有的页缓存机制POSIX 标准引入了mmap。Linux、Unix 以及类 Unix 系统都支持通过mmap将文件或设备映射到进程地址空间使进程可以像访问内存一样访问文件内容。2. mmap 的原理与详细机制2.1 核心思想mmap的本质是在内核中建立一种映射关系把文件的内容映射到进程的虚拟地址空间。映射建立后进程通过普通内存读写指令即可访问文件数据而不必再显式调用read / write。整个过程依赖操作系统的虚拟内存管理单元MMU和页缓存机制。2.2 详细机制调用mmap后内核并不会立刻把文件内容全部读入内存而是先创建一段虚拟内存区域记录该区域对应的文件、偏移量、映射长度、权限等信息。这个过程大致如下进程调用mmap内核在进程地址空间中找到一块合适的空闲虚拟地址区域内核创建VMAVirtual Memory Area记录映射的文件、页缓存、权限等元数据mmap返回虚拟地址但此时文件内容还没有真正加载进程第一次访问该虚拟地址时由于对应物理页尚未建立映射会触发缺页中断缺页处理程序根据VMA找到对应的文件页将文件内容从磁盘读入页缓存内核把进程虚拟页映射到页缓存所在的物理页进程随后再次访问该地址时即可直接读写页缓存中的内容。例如读取一个文件的前 4096 字节时内核可能只加载第一页其余部分仍然留在磁盘上直到被访问时才按需加载。这就是 mmap 的“惰性加载”机制。2.3 整体流程示意是否调用 open 打开文件调用 mmap 建立映射内核创建虚拟内存区域 VMA进程访问映射地址是否发生缺页中断从磁盘读入页缓存建立虚拟页与物理页映射进程读写内存调用 msync 同步脏页调用 munmap 解除映射2.4 页缓存复用传统read方式中磁盘数据先进入页缓存再拷贝到用户缓冲区而mmap映射建立后进程用户地址空间直接映射到页缓存。这样至少减少了一次从内核态到用户态的 CPU 拷贝因此常被称为“零拷贝”技术之一更准确地说是减少了 CPU 参与的数据拷贝。2.5 文件映射与匿名映射mmap可以映射两类完全不同的“对象”理解这一点能帮助区分它最常见的用法。文件映射把磁盘文件的内容映射到进程虚拟地址空间。映射建立后读写某个虚拟地址就等于读写文件对应位置的内容。它需要一个有效的文件描述符fd例如先open(data.txt, O_RDWR)再把fd传给mmap。匿名映射不关联任何文件只是请求内核分配一块初始内容为 0 的内存区域。因为没有对应的磁盘文件所以fd通常传-1并通过MAP_ANONYMOUS标志创建。常见用途是给进程分配大块内存、父子进程创建共享内存等。Linux 上还有更传统的匿名映射写法打开/dev/zero后映射它效果类似MAP_ANONYMOUS但现在推荐直接使用MAP_ANONYMOUS。2.6 共享映射与私有映射MAP_SHARED多个进程映射同一文件同一区域时对内存的修改会写回文件并被其他映射该文件的进程看到。MAP_PRIVATE对映射区域的修改不会写回文件而是触发写时复制COW。进程初次写入私有映射页时内核会复制一份页缓存页进程修改的是自己的副本。MAP_SHARED同时写一个文件会冲突吗会MAP_SHARED本身不提供同步保护。它只保证“所有映射最终看到同一份页缓存”但不保证多个进程并发写同一区域时结果正确。常见问题如下两个进程写同一区域时后写入的会覆盖先写入的内容一个进程正在更新文件时另一个进程可能读到中间状态如果进程同时用write()/lseek()和mmap操作同一个文件安全性和一致性更难保证。如何处理并发写冲突先加锁再进入关键区进程在修改MAP_SHARED区域前先对共享文件范围加flock()、fcntl()记录锁或使用共享内存中的 POSIX 互斥锁 / 信号量保证同一时刻只有一个进程写关键区域在锁保护下修改映射内存只有拿到锁的进程才去更新映射区域避免并发写相互覆盖也避免其他进程读到中间状态需要持久化时仍在持锁状态下调用msync(addr, len, MS_SYNC)把脏页同步写回文件确保数据落盘后再释放锁。因此正确的执行顺序是加锁 → 修改共享内存 →msync同步落盘 → 解锁。加锁解决并发写的一致性msync解决数据持久化而且msync应放在锁内执行避免“已解锁但仍未落盘”或“落盘过程中又发生并发写”的窗口问题。3. mmap、munmap、madvise 等接口说明以下接口声明主要参考 Linux 环境其他类 Unix 系统略有差异但语义基本一致。3.1 mmap函数原型#includesys/mman.hvoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);参数说明addr期望映射到的虚拟地址通常传 NULL由内核自动选择合适地址length映射长度单位字节内核会向上对齐到页大小prot内存保护权限可为以下值的按位或PROT_READ可读PROT_WRITE可写PROT_EXEC可执行PROT_NONE不可访问flags映射类型与行为MAP_SHARED共享映射写入会写回文件MAP_PRIVATE私有映射写入触发写时复制MAP_ANONYMOUS匿名映射不关联文件MAP_FIXED要求映射到 addr 指定的地址需谨慎使用MAP_POPULATE预填充页表减少后续缺页fd要映射的文件描述符匿名映射时通常传 -1offset文件内的偏移量必须是页大小的整数倍。返回值成功返回映射区域的起始地址失败返回MAP_FAILED并设置 errno。调用 mmap 后若文件大小小于映射长度访问超出文件实际大小的页可能触发 SIGBUS如果映射区域不可访问也可能触发 SIGSEGV。3.2 munmap函数原型#includesys/mman.hintmunmap(void*addr,size_tlength);作用解除 addr 开始的 length 字节的映射。解除后进程再访问该地址属于未定义行为。进程退出时所有映射也会被自动解除但显式调用 munmap 有利于及时释放地址空间和资源。返回值成功返回 0失败返回 -1 并设置 errno。3.3 msync函数原型#includesys/mman.hintmsync(void*addr,size_tlength,intflags);作用把 MAP_SHARED 映射中的脏页同步写回文件确保数据落盘。flags 可取值MS_ASYNC异步写回MS_SYNC同步写回调用返回时数据已经写入存储设备MS_INVALIDATE使其他映射该文件的缓存副本失效。对于需要保证数据持久化的场景应在修改共享映射后调用 msync。3.4 madvise函数原型#includesys/mman.hintmadvise(void*addr,size_tlength,intadvice);作用向内核提供映射区域的使用建议帮助内核优化页面调度。常用建议包括MADV_SEQUENTIAL程序将顺序访问该区域内核可以增加预读MADV_RANDOM程序将随机访问该区域内核可以减少预读MADV_WILLNEED建议内核提前载入指定页面减少后续缺页MADV_DONTNEED表示该区域近期不再需要内核可以回收对应页面MADV_NORMAL默认访问模式。madvise 只是优化建议不会影响程序正确性。3.5 其他相关接口mremap调整已有映射区的大小Linux 扩展接口mlock/munlock锁定映射区域防止被换出到交换分区remap_file_pagesLinux 早期提供的非线性文件映射接口现已不推荐使用。4. mmap 与 read 的对比下面从数据拷贝、系统调用、适用场景等角度对比 mmap 与 read。对比项readmmap数据拷贝次数通常涉及磁盘到页缓存、页缓存到用户缓冲区两次拷贝页缓存直接映射到用户地址空间减少一次 CPU 拷贝系统调用频率每次读取都需要调用 read大文件场景调用次数多建立映射后即可通过内存访问无需反复系统调用内存占用方式用户缓冲区显式分配数据按需读取依赖虚拟内存和页缓存按需加载对小文件随机访问每次 read 有固定调用开销通常更高效尤其是频繁随机访问对顺序大文件读写缓存命中率可控性能稳定可能触发大量缺页中断性能未必优于 read文件变化一致性每次 read 都能读到最新文件内容映射期间文件被外部截断可能导致 SIGBUS错误处理返回 -1 并设置 errno调用方可立即处理访问非法映射地址时可能触发 SIGSEGV 或 SIGBUS总体而言mmap适合频繁随机访问、进程间共享、需要把文件当内存使用的场景read更适合简单顺序读写、缓冲区较小的场景。实际选择时需要根据访问模式和文件大小进行测试。5. mmap 与 read 代码对比下面的示例分别演示使用 read 和 mmap 读取同一个文件并打印文件总字节数。代码基于 Linux / POSIX 接口可通过 gcc 编译运行。5.1 使用 read 读取文件#includestdio.h#includestdlib.h#includefcntl.h#includeunistd.h#includesys/stat.hintmain(intargc,char*argv[]){if(argc2){fprintf(stderr,Usage: %s file\n,argv[0]);return1;}intfdopen(argv[1],O_RDONLY);if(fd0){perror(open);return1;}structstatst;if(fstat(fd,st)0){perror(fstat);close(fd);return1;}size_ttotal0;charbuf[4096];ssize_tn0;while((nread(fd,buf,sizeof(buf)))0){total(size_t)n;}if(n0){perror(read);close(fd);return1;}printf(read total bytes: %zu\n,total);close(fd);return0;}5.2 使用 mmap 读取文件#includestdio.h#includestdlib.h#includefcntl.h#includeunistd.h#includesys/stat.h#includesys/mman.hintmain(intargc,char*argv[]){if(argc2){fprintf(stderr,Usage: %s file\n,argv[0]);return1;}intfdopen(argv[1],O_RDONLY);if(fd0){perror(open);return1;}structstatst;if(fstat(fd,st)0){perror(fstat);close(fd);return1;}if(st.st_size0){printf(mmap total bytes: 0\n);close(fd);return0;}void*addrmmap(NULL,(size_t)st.st_size,PROT_READ,MAP_PRIVATE,fd,0);if(addrMAP_FAILED){perror(mmap);close(fd);return1;}// 建立映射后可以像访问内存一样访问文件内容。// 这里仅演示访问映射区域例如读取第一个字节。volatileunsignedcharfirst_byte((unsignedchar*)addr)[0];(void)first_byte;printf(mmap total bytes: %lld\n,(longlong)st.st_size);if(munmap(addr,(size_t)st.st_size)0){perror(munmap);close(fd);return1;}close(fd);return0;}5.3 代码对比说明read 示例需要手动维护缓冲区并循环调用 read直到读完整个文件mmap 示例只需要调用一次 mmap 建立映射之后就可以用指针或数组方式访问文件内容访问结束后调用 munmap 解除映射mmap 方式代码更接近内存操作适合随机访问场景read 方式对顺序读取更直观也更容易控制每次读取大小。6. 注意事项与总结使用 mmap 时需要注意以下几点对 MAP_SHARED 映射的修改不会立即写回磁盘需要调用 msync 或等内核回写映射期间如果文件被其他进程或程序截断访问被截断的页可能触发 SIGBUSoffset 必须是页大小的整数倍否则 mmap 会失败匿名映射通常用MAP_PRIVATE | MAP_ANONYMOUS在父子进程间可共享内存但写时复制后各自独立32 位系统地址空间有限超大文件可能无法一次性完整映射mmap 建立映射需要占用虚拟地址空间大量小映射也会带来 VMA 管理开销。总的来说mmap 通过把文件映射到进程地址空间复用了操作系统的页缓存机制减少了传统 read 方式中的 CPU 拷贝和系统调用频率。它不是所有场景下都优于 read但在随机访问、进程间共享、文件当内存使用等场景中非常有效。理解 background、缺页机制、共享与私有映射的差异以及 mmap、munmap、msync、madvise 等接口的语义是高效使用 mmap 的关键。

相关新闻

FANUC 0i-MF数控系统操作指南:面板、对刀、程序传输与报警排查

FANUC 0i-MF数控系统操作指南:面板、对刀、程序传输与报警排查

简介:数控机床的操作核心在于理解系统逻辑与坐标关系。FANUC 0i-MF作为常见数控系统,操作员需掌握模式选择、倍率联动、对刀与坐标系写入等基础环节,才能安全完成首件试切。程序传输涉及U盘导入与DNC在线加工,需注意文件格式和通信…

2026/10/14 17:27:51 阅读更多 →
1MB轻量人脸检测网络ULGFD结构拆解与ARM部署实战

1MB轻量人脸检测网络ULGFD结构拆解与ARM部署实战

前阵子接到一个边缘盒子上做人脸检测的需求,硬件平台是一颗不算强的ARM处理器,内存也只有几百MB,客户要求检测速度必须跑满实时,还不能占用太多资源。我第一反应是上轻量化的YOLO系列,但试了一圈发现要么模型太大&…

2026/10/14 17:27:51 阅读更多 →
AI 代码审计和静态分析的检测覆盖差在哪?2026 年技术路线对比

AI 代码审计和静态分析的检测覆盖差在哪?2026 年技术路线对比

本文为安全研究团队的技术实践总结,文中涉及环境均为自有系统或已获授权的测试目标。静态分析依靠语法与数据流的规则判断,AI 代码审计依靠模型对代码语义与上下文的理解,两者的覆盖差异集中体现在跨文件、跨流程与业务约束三类场景。 一、两…

2026/10/14 17:27:51 阅读更多 →

最新新闻

Linux下C/C++动静态库制作与使用实战指南

Linux下C/C++动静态库制作与使用实战指南

长期以来在Linux下做C/C开发,动静态库始终是绕不开的一个核心技能。无论是自己封装模块、分发二进制产物,还是处理第三方依赖,理解库的制作与使用直接决定了你在编译链接这块能不能少掉头发。我见过不少开发者写业务逻辑很熟练,一…

2026/10/15 4:26:50 阅读更多 →
Flutter鸿蒙开发实战:星座应用跨平台全流程解析

Flutter鸿蒙开发实战:星座应用跨平台全流程解析

1. 项目背景与整体设计思路1.1 为什么选 Flutter 做鸿蒙开发聊到跨平台开发,Flutter 这几年的热度一直在线。原因很简单——一套 Dart 代码,同时跑在 Android、iOS、Windows、macOS、Linux 上,现在还要加上鸿蒙。放在以前,鸿蒙应用…

2026/10/15 4:26:50 阅读更多 →
线上故障排查实战指南:从告警到恢复的黄金步骤

线上故障排查实战指南:从告警到恢复的黄金步骤

半夜两点被电话叫醒,睁开眼看到屏幕上一排红色告警,心跳直接拉满。这种场景,几乎所有做过线上系统的开发者都经历过。告警本身不可怕,可怕的是大脑一片空白,不知道该先点哪里,也不知道哪些命令能救命。这篇…

2026/10/15 4:26:50 阅读更多 →
基于SpringBoot+Vue的个人博客系统开发实战

基于SpringBoot+Vue的个人博客系统开发实战

1. 项目概述与整体技术选型1.1 这个博客系统到底解决了什么问题做个人博客这件事,看起来简单,真做起来坑不少。早期用现成框架,比如WordPress或者各类静态站点生成器,总觉得差了点什么——要么太重,装一堆插件才能用&a…

2026/10/15 4:26:50 阅读更多 →
共享内存IPC实战:从零拷贝原理到信号量同步,提升跨进程通信性能

共享内存IPC实战:从零拷贝原理到信号量同步,提升跨进程通信性能

共享内存(shm)这个老牌IPC方案,我最近在一个数据采集服务里又把它翻了出来。这个服务原本是跨进程传数据,管道和Unix域Socket用得挺顺手,可流量一起来,CPU占用就再也压不下去。换成共享内存之后&#xff0c…

2026/10/15 4:26:50 阅读更多 →
如何判断Ubuntu是否真的关机?三路检查方法与实战探测脚本

如何判断Ubuntu是否真的关机?三路检查方法与实战探测脚本

你有没有遇到过这种情况:在终端里输入了sudo shutdown now,屏幕上日志飞快滚动,最后一切归于黑暗,风扇也慢慢停转,你很自然地认为这台 Ubuntu 机器已经关机了。但我在几年的实践中发现,这个结论经常下得太早…

2026/10/15 4:25:50 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →