草莓熊Lotso个人主页❄️个人专栏:《C知识分享》 《Linux 入门到实践零基础也能懂》✨生活是默默的坚持毅力是永久的享受 博主简介文章目录前言一. mmap 到底是什么1.1 核心优势1.2 映射的内存布局二. mmap 与 munmap API 全解析2.1 函数原型2.2 mmap 参数介绍2.3 返回值说明2.4 核心标志位深度辨析MAP_SHARED vs MAP_PRIVATE三. mmap 实战开发PDF 完整代码复刻 详细注释3.1 实战 1基于 mmap 的文件写入3.2 实战 2基于 mmap 的文件读取3.3 实战 3用 mmap 极简模拟 malloc/free 实现四. mmap 使用避坑指南开发必看五. 传统 read/write vs mmap 怎么选仅供参考结尾前言大家好我是深耕 Linux 内核与系统开发的博主。在 Linux 高性能开发中mmap是一个极具魔力的系统调用 —— 它能让我们直接通过内存操作读写文件省去传统read/write的内核态与用户态数据拷贝开销还能实现进程间共享内存、自定义内存分配等高级功能。本文从核心原理、API 参数、实战代码到避坑指南全覆盖所有代码均可直接编译运行兼顾学习理解与工业级开发参考。一. mmap 到底是什么mmap全称memory map即内存映射是 Linux 提供的系统调用核心能力是将一个文件或设备的内容直接映射到进程的虚拟地址空间中。映射完成后进程对这段虚拟内存的读写操作会被内核自动同步到对应的文件 / 设备上无需再调用传统的read/write系统调用。1.1 核心优势零拷贝高效访问传统read/write需要先把数据从磁盘拷贝到内核缓冲区再拷贝到用户态内存而mmap直接建立文件与用户虚拟地址的映射只需要一次拷贝大幅提升大文件读写效率。统一访问形式操作文件就像操作内存一样直接通过指针读写无需繁琐的文件偏移操作。天然支持共享内存多个进程映射同一个文件可直接实现进程间数据共享是 Linux 进程间通信IPC的经典实现方式。灵活的内存管理可实现匿名映射用于自定义内存分配替代malloc的部分场景。1.2 映射的内存布局在进程的虚拟地址空间中mmap的映射区域位于堆区和栈区之间的共享区mmap 区域和动态库的加载区域一致。二. mmap 与 munmap API 全解析2.1 函数原型#includesys/mman.h// 创建内存映射void*mmap(void*addr,size_t length,intprot,intflags,intfd,off_t offset);// 解除内存映射intmunmap(void*addr,size_t length);2.2 mmap 参数介绍2.3 返回值说明mmap成功返回指向映射区域起始地址的指针mmap失败返回MAP_FAILED即(void *)-1并设置errno指示错误原因munmap成功返回 0munmap失败返回 - 1并设置errno。2.4 核心标志位深度辨析MAP_SHARED vs MAP_PRIVATE这是mmap最核心的两个标志位决定了映射的行为模式必须分清特性MAP_SHARED共享映射MAP_PRIVATE私有映射修改同步对内存的修改会同步到底层文件修改不会同步到文件触发写时拷贝多进程可见对其他映射同一文件的进程可见对其他进程不可见修改仅当前进程有效适用场景进程间共享内存、大文件读写修改只读文件映射、私有内存分配、不希望修改源文件的场景三. mmap 实战开发PDF 完整代码复刻 详细注释3.1 实战 1基于 mmap 的文件写入该示例通过mmap映射文件直接向映射内存写入数据无需write系统调用数据会自动同步到文件。关键注意事项要实现写入映射文件必须以O_RDWR模式打开读写模式空文件无法直接映射必须通过ftruncate设置文件大小保证映射的长度有对应的文件存储空间映射长度必须是页大小整数倍。#includeiostream#includefcntl.h#includesys/mman.h#includesys/stat.h#includeunistd.h#includesys/types.hconstintPAGE_SIZE4096;// 其实最后最小都是 4096,一定要是4096的倍数,否则会报错// write_mmap filenameintmain(intargc,char*argv[]){if(argc!2){std::cerrUsage: argv[0] filenamestd::endl;return1;}// 1.打开目标文件, mmap需要自己先打开文件intfd::open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666);if(fd0){std::cerrFailed to open file: argv[1]std::endl;return2;}// 2. 我们需要手动调整一个文件的大小,方便我们进行合法的mmapif(::ftruncate(fd,PAGE_SIZE)0){std::cerrFailed to ftruncate file: argv[1]std::endl;return3;}// 3. 进行mmap操作char*shmaddr(char*)::mmap(NULL,PAGE_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(shmaddrMAP_FAILED){std::cerrFailed to mmap file: argv[1]std::endl;return4;}// 4. 正在进行文件操作for(charca;cz;c){shmaddr[c-a]c;sleep(1);}// 5. 关闭文件映射if(::munmap(shmaddr,PAGE_SIZE)-1){std::cerrFailed to munmap file: argv[1]std::endl;return5;}// 6. 关闭文件描述符::close(fd);return0;}3.2 实战 2基于 mmap 的文件读取该示例通过mmap映射已有文件直接读取映射内存即可获取文件内容无需read系统调用。#includeiostream#includefcntl.h#includesys/mman.h#includesys/stat.h#includeunistd.h#includesys/types.hconstintPAGE_SIZE4096;// 其实最后最小都是 4096,一定要是4096的倍数,否则会报错// read_mmap filenameintmain(intargc,char*argv[]){if(argc!2){std::cerrUsage: argv[0] filenamestd::endl;return1;}// 1.打开目标文件, mmap需要自己先打开文件intfd::open(argv[1],O_RDONLY);if(fd0){std::cerrFailed to open file: argv[1]std::endl;return2;}// 2. 获取文件的大小structstatst;if(::fstat(fd,st)0){std::cerrFailed to fstat file: argv[1]std::endl;return3;}// 3. 进行mmap操作char*shmaddr(char*)::mmap(NULL,st.st_size,PROT_READ,MAP_SHARED,fd,0);if(shmaddrMAP_FAILED){std::cerrFailed to mmap file: argv[1]std::endl;return4;}// 4. 正在进行文件操作std::coutshmaddrstd::endl;// 5. 关闭文件映射if(::munmap(shmaddr,st.st_size)-1){std::cerrFailed to munmap file: argv[1]std::endl;return5;}// 6. 关闭文件描述符::close(fd);return0;}哎为啥没读到我们后面之前填充的那些东西呢因为那些是用的0值填充3.3 实战 3用 mmap 极简模拟 malloc/free 实现malloc的底层实现在分配大内存时本质就是通过mmap的匿名映射实现的。我们可以通过mmapmunmap极简模拟malloc和free的核心功能。核心原理匿名映射通过MAP_PRIVATE|MAP_ANONYMOUS标志创建不关联任何文件仅分配一段私有的空白内存my_malloc调用mmap分配指定大小的内存返回内存首地址my_free调用munmap释放映射的内存。#includeiostream#includecstdio#includecstring#includefcntl.h#includesys/mman.h#includesys/stat.h#includeunistd.h#includesys/types.h// 极简malloc实现void*my_malloc(size_t size){if(size0){void*addr(void*)::mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);if(addrMAP_FAILED){std::cerrFailed to mmap sizestd::endl;returnnullptr;}returnaddr;}returnnullptr;}voidmy_free(void*start,size_t size){if(start!nullptrsize0){intret::munmap(start,size);if(ret-1){std::cerrFailed to munmap sizestd::endl;}}}intmain(){char*p(char*)my_malloc(1024);if(pnullptr){std::cerrFailed to malloc 1024 bytesstd::endl;return1;}// 使用分配的内存,简单打印指针值printf(Allocated memory at address: %p\n,p);// 在这里使用ptr指向的内存memset(p,A,1024);for(inti0;i1024;i){printf(%c ,p[i]);fflush(stdout);sleep(1);}// 释放内存my_free(p,1024);return0;}进阶验证gdb 查看内存映射我们可以通过 gdb 调试查看mmap前后进程的地址空间映射变化# 带调试信息编译 gcc-g my_malloc.c-o my_malloc#gdb调试gdb./my_malloc在 gdb 中执行以下命令# 在printf分配地址处打断点 b39# 运行程序 r # 查看映射前的地址空间 info proc mapping # 单步执行完成mmap n # 再次查看地址空间能看到新增的mmap匿名映射区域 info proc mapping可以清晰看到mmap后进程的地址空间中新增了一段匿名映射区域就是我们分配的内存。四. mmap 使用避坑指南开发必看必须保证页大小对齐length和offset必须是系统页大小的整数倍否则会调用失败可通过sysconf(_SC_PAGESIZE)获取系统真实页大小不要硬编码 4KB。文件打开权限与映射权限必须匹配要设置PROT_WRITE可写权限文件必须以O_RDWR模式打开仅O_WRONLY或O_RDONLY会映射失败只读映射PROT_READ文件至少要有O_RDONLY权限。空文件必须提前设置大小空文件大小为 0直接映射会触发总线错误SIGBUS必须通过ftruncate/lseekwrite提前给文件分配足够的空间再进行映射。映射解除后禁止再访问调用munmap后映射区域会被回收再访问该地址会触发段错误SIGSEGV。MAP_SHARED 修改同步时机共享映射的修改不会实时同步到磁盘内核会根据脏页刷新策略自动同步若需要强制同步可调用msync函数主动刷盘。线程安全问题多个进程 / 线程同时修改共享映射的同一块内存会出现竞态条件需要通过信号量、互斥锁做同步。五. 传统 read/write vs mmap 怎么选仅供参考特性read/writemmap数据拷贝2 次拷贝磁盘→内核缓冲区→用户态1 次拷贝磁盘→用户内存随机访问效率低需要频繁 lseekread效率高直接指针偏移访问大文件处理内存占用低适合流式读写性能优势极大适合随机读写小文件处理开销小使用简单有页大小对齐的内存浪费优势不明显编程复杂度简单接口易用相对复杂需要处理对齐、权限等问题异常处理系统调用返回错误不会直接崩溃非法访问会触发 SIGBUS/SIGSEGV直接终止进程最佳选择建议✅ 大文件随机读写、频繁修改文件内容优先选mmap✅ 进程间共享内存、多进程通信必须用mmap共享映射✅ 自定义内存分配、大块内存申请用mmap匿名映射❌ 小文件一次性流式读写、顺序读写用read/write更简单❌ 对程序稳定性要求极高不能接受崩溃的场景优先read/write异常处理更可控。结尾 我是草莓熊 Lotso若这篇技术干货帮你打通了学习中的卡点 【关注】跟我一起深耕技术领域从基础到进阶见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好需要时直接查、随时用 【评论】分享你的经验或疑问比如曾踩过的技术坑一起交流避坑 ️ 【投票】用你的选择助力社区内容方向告诉大家哪个技术点最该重点拆解 技术之路难免有困惑但同行的人会让前进更有方向愿我们都能在自己专注的领域里一步步靠近心中的技术目标结语mmap是 Linux 系统开发中极具威力的工具它打破了 “文件操作” 和 “内存操作” 的壁垒既能实现高性能的文件读写又能完成进程间共享内存、自定义内存管理等高级功能。本文完整覆盖了 mmap 的核心原理、API、实战代码和避坑指南无论是学习理解还是开发参考都能直接使用。后续我会继续分享基于 mmap 的 LRU 缓存实现、进程间共享内存通信等进阶内容欢迎点赞、收藏、关注一起深耕 Linux 系统开发✨把这些内容吃透超牛的放松下吧✨ʕ˘ᴥ˘ʔづきらど