
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一. mmap 到底是什么?](#一. mmap 到底是什么?)
-
- [1.1 核心优势](#1.1 核心优势)
- [1.2 映射的内存布局](#1.2 映射的内存布局)
- [二. mmap 与 munmap API 全解析](#二. mmap 与 munmap API 全解析)
-
- [2.1 函数原型](#2.1 函数原型)
- [2.2 mmap 参数介绍](#2.2 mmap 参数介绍)
- [2.3 返回值说明](#2.3 返回值说明)
- [2.4 核心标志位深度辨析:MAP_SHARED vs MAP_PRIVATE](#2.4 核心标志位深度辨析:MAP_SHARED vs MAP_PRIVATE)
- [三. mmap 实战开发(PDF 完整代码复刻 + 详细注释)](#三. mmap 实战开发(PDF 完整代码复刻 + 详细注释))
-
- [3.1 实战 1:基于 mmap 的文件写入](#3.1 实战 1:基于 mmap 的文件写入)
- [3.2 实战 2:基于 mmap 的文件读取](#3.2 实战 2:基于 mmap 的文件读取)
- [3.3 实战 3:用 mmap 极简模拟 malloc/free 实现](#3.3 实战 3:用 mmap 极简模拟 malloc/free 实现)
- [四. mmap 使用避坑指南(开发必看)](#四. mmap 使用避坑指南(开发必看))
- [五. 传统 read/write vs 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 函数原型
cpp
#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);
2.2 mmap 参数介绍


2.3 返回值说明
mmap成功:返回指向映射区域起始地址的指针;mmap失败:返回MAP_FAILED(即(void *)-1),并设置errno指示错误原因;munmap成功:返回 0;munmap失败:返回 - 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设置文件大小,保证映射的长度有对应的文件存储空间;
- 映射长度必须是页大小整数倍。
cpp
#include<iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
const int PAGE_SIZE = 4096; // 其实最后最小都是 4096,一定要是4096的倍数,否则会报错
// write_mmap filename
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
return 1;
}
// 1.打开目标文件, mmap需要自己先打开文件
int fd = ::open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
std::cerr << "Failed to open file: " << argv[1] << std::endl;
return 2;
}
// 2. 我们需要手动调整一个文件的大小,方便我们进行合法的mmap
if(::ftruncate(fd, PAGE_SIZE) < 0)
{
std::cerr << "Failed to ftruncate file: " << argv[1] << std::endl;
return 3;
}
// 3. 进行mmap操作
char *shmaddr = (char*)::mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(shmaddr == MAP_FAILED)
{
std::cerr << "Failed to mmap file: " << argv[1] << std::endl;
return 4;
}
// 4. 正在进行文件操作
for (char c = 'a'; c <= 'z'; c++)
{
shmaddr[c - 'a'] = c;
sleep(1);
}
// 5. 关闭文件映射
if(::munmap(shmaddr, PAGE_SIZE) == -1)
{
std::cerr << "Failed to munmap file: " << argv[1] << std::endl;
return 5;
}
// 6. 关闭文件描述符
::close(fd);
return 0;
}



3.2 实战 2:基于 mmap 的文件读取
该示例通过mmap映射已有文件,直接读取映射内存即可获取文件内容,无需read系统调用。
cpp
#include<iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
const int PAGE_SIZE = 4096; // 其实最后最小都是 4096,一定要是4096的倍数,否则会报错
// read_mmap filename
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
return 1;
}
// 1.打开目标文件, mmap需要自己先打开文件
int fd = ::open(argv[1], O_RDONLY);
if(fd < 0)
{
std::cerr << "Failed to open file: " << argv[1] << std::endl;
return 2;
}
// 2. 获取文件的大小
struct stat st;
if(::fstat(fd, &st) < 0)
{
std::cerr << "Failed to fstat file: " << argv[1] << std::endl;
return 3;
}
// 3. 进行mmap操作
char *shmaddr = (char*)::mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if(shmaddr == MAP_FAILED)
{
std::cerr << "Failed to mmap file: " << argv[1] << std::endl;
return 4;
}
// 4. 正在进行文件操作
std::cout << shmaddr << std::endl;
// 5. 关闭文件映射
if(::munmap(shmaddr, st.st_size) == -1)
{
std::cerr << "Failed to munmap file: " << argv[1] << std::endl;
return 5;
}
// 6. 关闭文件描述符
::close(fd);
return 0;
}

哎,为啥没读到我们后面之前填充的那些东西呢,因为那些是用的0值填充
3.3 实战 3:用 mmap 极简模拟 malloc/free 实现
malloc的底层实现,在分配大内存时,本质就是通过mmap的匿名映射实现的。我们可以通过mmap+munmap,极简模拟malloc和free的核心功能。
核心原理
- 匿名映射 :通过
MAP_PRIVATE|MAP_ANONYMOUS标志创建,不关联任何文件,仅分配一段私有的空白内存; my_malloc:调用mmap分配指定大小的内存,返回内存首地址;my_free:调用munmap释放映射的内存。
cpp
#include<iostream>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
// 极简malloc实现
void* my_malloc(size_t size)
{
if(size > 0)
{
void* addr = (void*)::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(addr == MAP_FAILED)
{
std::cerr << "Failed to mmap " << size << std::endl;
return nullptr;
}
return addr;
}
return nullptr;
}
void my_free(void* start, size_t size)
{
if(start != nullptr && size > 0)
{
int ret = ::munmap(start, size);
if(ret == -1)
{
std::cerr << "Failed to munmap " << size << std::endl;
}
}
}
int main()
{
char* p = (char*)my_malloc(1024);
if(p == nullptr)
{
std::cerr << "Failed to malloc 1024 bytes" << std::endl;
return 1;
}
// 使用分配的内存,简单打印指针值
printf("Allocated memory at address: %p\n", p);
// 在这里使用ptr指向的内存
memset(p, 'A', 1024);
for(int i = 0; i < 1024; i++)
{
printf("%c ", p[i]);
fflush(stdout);
sleep(1);
}
// 释放内存
my_free(p, 1024);
return 0;
}

进阶验证:gdb 查看内存映射
我们可以通过 gdb 调试,查看mmap前后进程的地址空间映射变化:
cpp
# 带调试信息编译
gcc -g my_malloc.c -o my_malloc
# gdb调试
gdb ./my_malloc
在 gdb 中执行以下命令:
cpp
# 在printf分配地址处打断点
b 39
# 运行程序
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/lseek+write提前给文件分配足够的空间,再进行映射。
-
映射解除后禁止再访问
- 调用
munmap后,映射区域会被回收,再访问该地址会触发段错误(SIGSEGV)。
- 调用
-
MAP_SHARED 修改同步时机
- 共享映射的修改不会实时同步到磁盘,内核会根据脏页刷新策略自动同步;
- 若需要强制同步,可调用
msync函数主动刷盘。
-
线程安全问题
- 多个进程 / 线程同时修改共享映射的同一块内存,会出现竞态条件,需要通过信号量、互斥锁做同步。
五. 传统 read/write vs mmap 怎么选?(仅供参考)
| 特性 | read/write | mmap |
|---|---|---|
| 数据拷贝 | 2 次拷贝(磁盘→内核缓冲区→用户态) | 1 次拷贝(磁盘→用户内存) |
| 随机访问 | 效率低,需要频繁 lseek+read | 效率高,直接指针偏移访问 |
| 大文件处理 | 内存占用低,适合流式读写 | 性能优势极大,适合随机读写 |
| 小文件处理 | 开销小,使用简单 | 有页大小对齐的内存浪费,优势不明显 |
| 编程复杂度 | 简单,接口易用 | 相对复杂,需要处理对齐、权限等问题 |
| 异常处理 | 系统调用返回错误,不会直接崩溃 | 非法访问会触发 SIGBUS/SIGSEGV,直接终止进程 |
最佳选择建议
- ✅ 大文件随机读写、频繁修改文件内容:优先选
mmap; - ✅ 进程间共享内存、多进程通信:必须用
mmap共享映射; - ✅ 自定义内存分配、大块内存申请:用
mmap匿名映射; - ❌ 小文件一次性流式读写、顺序读写:用
read/write更简单; - ❌ 对程序稳定性要求极高,不能接受崩溃的场景:优先
read/write,异常处理更可控。
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:mmap是 Linux 系统开发中极具威力的工具,它打破了 "文件操作" 和 "内存操作" 的壁垒,既能实现高性能的文件读写,又能完成进程间共享内存、自定义内存管理等高级功能。本文完整覆盖了 mmap 的核心原理、API、实战代码和避坑指南,无论是学习理解还是开发参考,都能直接使用。后续我会继续分享基于 mmap 的 LRU 缓存实现、进程间共享内存通信等进阶内容,欢迎点赞、收藏、关注,一起深耕 Linux 系统开发!
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
