
我们先明确两个目标:一是理解 mmap 文件映射 的基本原理,二是掌握 mmap 的基本用法。
mmap 介绍
在正式讲解 mmap 之前,先做一个基本介绍。mmap 是 Linux 系统提供的一个系统调用 ,它可以让一个进程把一个已经打开的文件,映射到自己的虚拟地址空间中。映射完成后,系统会返回给用户一个虚拟地址。从此以后,我们对文件内容的读取、修改、写入等操作,不再需要使用 read、write 这类系统调用,直接通过这个虚拟地址就可以对文件内容进行编辑,这就是 mmap 文件映射的核心作用。当然 mmap 还有其他功能,但现阶段我们先聚焦在文件映射上。

所以我们再对比一下传统文件操作和 mmap 的区别。以前读写文件,都是先打开文件得到文件描述符 fd,再通过 read、write 进行读写。这种方式必须经过系统调用,数据需要在用户空间和内核空间之间来回拷贝。而如果我们把文件对应的内核缓冲区,直接与进程地址空间建立映射,就可以直接用虚拟地址访问文件 ,这就是 mmap 的设计思路。
可以这样理解:当前进程通过 mmap 系统调用,把被打开文件在内存中的缓冲区(数据部分)映射到自己的地址空间,之后就可以用虚拟地址直接读写文件内容。看到这里,大家应该会想到之前学过的 System V 共享内存 ------mmap 本质上也是一种共享内存机制。
因为文件打开后对应的缓冲区本身就是内存块,把这块内存映射到进程地址空间,就是文件映射,也是内存映射。如果多个进程都通过 mmap 把同一块文件内存映射到各自地址空间,就实现了进程间共享内存,原理和 System V 共享内存完全一致。我们本次重点先放在文件映射 上。
所以我们应该可以提前知道:传统文件读写(read/write)流程是这样的:
- 进程发起
read系统调用 - 数据从磁盘 → 内核缓冲区
- 数据再从内核缓冲区 → 用户缓冲区
- 进程才能使用数据
你会发现:必须多做一次「内核 → 用户」的数据拷贝。而且频繁调用系统调用本身也有开销。
而 mmap 文件映射 的流程完全不同:
- 进程调用 mmap,把文件内核缓冲区直接映射到进程地址空间
- 进程拿到虚拟地址,直接访问内核里的文件数据
- 没有任何多余的数据拷贝
这就相当于:
- 传统方式:你要拿文件,必须让内核先拿一遍,再递给你
- mmap 方式:内核直接把文件 "放在你家门口",你伸手就能用
正是因为省去了数据拷贝,mmap 才会比传统文件操作更快、效率更高!
我们看看具体的系统调用长什么样子:
cpp
// 头文件:使用mmap/munmap必须包含的系统头文件
#include <sys/mman.h>
// 函数功能:将文件或设备映射到进程的虚拟内存地址空间
// 返回值:成功返回指向映射区域的指针;失败返回 MAP_FAILED (void*)-1
void *mmap(
void *addr, // 建议映射的起始地址,填NULL由系统自动分配(推荐)
size_t length, // 要映射的字节长度,必须是系统页大小的整数倍
int prot, // 内存保护权限:PROT_READ/PROT_WRITE/PROT_EXEC等
int flags, // 映射标志:MAP_SHARED(共享) / MAP_PRIVATE(私有) 必选其一
int fd, // 要映射的文件描述符(已打开的文件)
off_t offset // 文件映射的起始偏移量,必须是页大小整数倍
);
// 函数功能:取消内存映射,释放映射区域
// 返回值:成功返回0;失败返回-1
int munmap(
void *addr, // mmap返回的映射起始地址
size_t length // 要取消映射的长度,与mmap的length一致
);
使用 mmap 需要包含头文件 <sys/mman.h>,mmap 有多个参数,后面会逐一展开。它的核心作用,就是把一个已经打开的文件,映射到进程的地址空间中。
基本说明
- 允许用户空间程序将文件或设备的内容直接映射到进程的虚拟地址空间中。通过 mmap,程序可以高效地访问文件数据,而无需通过传统的 read 或 write 系统调用进行数据的复制
- mmap 还可以用于实现共享内存,允许不同进程间共享数据
参数介绍
void *addr:一个提示地址,表示希望映射区域开始的地址。然而,这个地址可能会被内核忽略,特别是当我们没有足够的权限来请求特定的地址时。如果 addr 是 NULL,则系统会自动选择一个合适的地址【设为 NULL,表示由系统自动选择合适地址,这也是我们推荐的用法,返回值就是系统选定的地址】
size_t length:要映射到进程地址空间中的字节数。这个长度必须是系统页面大小的整数倍(通常是 4KB,但可能因系统而异)。如果指定的 length 不是页面大小的整数倍,系统可能会向上舍入到最近的页面大小(系统内存页大小为 4KB(即 4096 字节)),而请求的内存大小为 3500 字节,则按照向上舍入的原则,应分配 4096 字节的内存
int prot:指定了映射区域的内存保护属性。可以是以下值的组合(使用按位或运算符 |):
PROT_READ:映射区域可读。PROT_WRITE:映射区域可写。PROT_EXEC:映射区域可执行。
int flags:指定了映射的类型和其他选项
MAP_PRIVATE:创建一个私有映射。对映射区域的修改不会反映到底层文件中。MAP_SHARED:创建一个共享映射。对映射区域的修改会反映到底层文件中(前提是文件是以写方式打开的,并且文件系统支持这种操作)。- 其他选项(如
MAP_ANONYMOUS、MAP_ANONYMOUS_SHARED等)可能也存在于某些系统上,用于创建不与文件关联的匿名映射。
cpp
/*
* flags 参数决定了映射区的修改行为:
* 1. 其他进程能不能看到修改
* 2. 修改会不会同步到底层文件
* 规则:必须从下面选 **且只能选一个**
*/
// 共享映射(最常用、最重要)
MAP_SHARED
共享这个映射。
1. 你对映射区的修改,**其他映射同一块区域的进程能立刻看到**
2. 如果是文件映射,修改**会直接写回磁盘文件**
3. 精确控制什么时候写回文件,需要用 msync(2)
// 共享映射(带校验,Linux 4.15+)
MAP_SHARED_VALIDATE
和 MAP_SHARED 功能完全一样。
区别:
MAP_SHARED 会忽略你传入的无效标志
MAP_SHARED_VALIDATE 会**严格检查所有标志**,发现不认识的标志直接报错 EOPNOTSUPP
一些特殊标志(如 MAP_SYNC)必须配合它使用
// 私有映射(写时复制)
MAP_PRIVATE
创建私有"写时复制"映射。
1. 你对映射区的修改,**其他进程完全看不见**
2. 修改**绝对不会写回磁盘文件**
3. 不保证 mmap 之后文件发生的改动,会反映到映射区里
int fd:一个有效的文件描述符,指向要映射的文件或设备。对于匿名映射,这个参数可以是 -1(在某些系统上,也可以使用MAP_ANONYMOUS或MAP_ANON标志来指定匿名映射,此时 fd 参数会被忽略)off_t offset:文件中的起始偏移量,即映射区域的开始位置。offset和length一起定义了映射区域在文件中的位置和大小。
返回值
cpp
/*
* RETURN VALUE
* 返回值说明
*/
// mmap 返回值
On success, mmap() returns a pointer to the mapped area.
成功时,mmap() 返回指向映射区域的指针。
On error, the value MAP_FAILED (that is, (void *) -1) is returned, and errno is set to indicate the cause of the error.
失败时,返回 MAP_FAILED(也就是 (void*)-1),并设置 errno 以表明错误原因。
// munmap 返回值
On success, munmap() returns 0.
成功时,munmap() 返回 0。
On failure, it returns -1, and errno is set to indicate the cause of the error (probably to EINVAL).
失败时,返回 -1,并设置 errno 以表明错误原因(最常见是 EINVAL)。
mmap 的返回值:成功时返回映射区域的起始虚拟地址;
失败则返回 MAP_FAILED,本质是 (void*)-1。
demo 代码
写入映射
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
#define SIZE 4096
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " filemame" << std::endl;
return 1;
}
std::string filename = argv[1];
// 注意:要成功进行写入映射,这里打开文件的模式必须是:O_RDWR
int fd = ::open(filename.c_str(), O_CREAT | O_RDWR, 0666);
if(fd < 0)
{
std::cerr << "open error" << std::endl;
return 2;
}
// 默认文件大小是0,无法和mmap进行正确映射,这里需要调整文件大小,用0值填充
::ftruncate(fd, SIZE);
char *mmap_addr = (char*):mmap(nullptr, SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if(mmap_addr == MAP_FAILED)
{
perror("mmap");
return 3;
}
// 操作文件
for(int i = 0; i < SIZE; i++)
{
mmap_addr[i] = 'a' + i%26;
}
// memcpy(mmap_addr, "hello", 5);
// 取消映射
::munmap(mmap_addr, SIZE);
// 关闭文件
::close(fd);
return 0;
}
我们代码里已经包含了 <unistd.h>、<sys/types.h> 这些头文件,接下来要用到 ftruncate 函数,它的作用就是把指定文件调整为指定大小 。我们直接用它把目标文件调整为我们定义的 CONFIG_SIZE 大小,也就是一页 4096 字节。
映射完成后,就可以直接操作文件了,不再需要任何 read/write 系统调用 ,直接用 mmap 返回的虚拟地址 shmaddr 读写就行。
我们可以做一个简单测试:循环从字符 'a' 写到 'z',每隔一秒写入一个字符,并用 sleep(1) 做延迟。注意不要移动指针地址,因为后面取消映射 munmap 时,必须使用最开始的起始地址。
写完代码后进行编译测试:用 g++ 编译,指定 C++11 标准,生成可执行程序。运行时必须传入文件名,比如 ./a.out log.txt。运行后,当前目录会生成一个 log.txt,大小正好是 4096 字节。我们用 cat 命令查看文件内容,会发现内容每隔一秒不断增加,这就是 mmap 直接写入的效果 ------ 数据先写到内存映射区,系统定期刷新到磁盘文件。程序跑完后,文件里会写入 26 个英文字母,虽然我们只写了 26 字节,但文件大小依旧是 4096 字节,后面未覆盖的区域保持默认值,这就是完整的文件映射写入过程。
基于这个原理,即使是 10G 的大文件,我们也可以用 mmap 映射到内存,然后拆分成 1000 个小文件分别管理,把大文件管理转化为内存块管理,这就是 mmap 的强大之处。
读取映射
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " filemame" << std::endl;
return 1;
}
std::string filename = argv[1];
// 注意:要成功进行写入映射,这里打开文件的模式必须是:O_RDWR
int fd = ::open(filename.c_str(), O_RDONLY);
if(fd < 0)
{
std::cerr << "open error" << std::endl;
return 2;
}
// 获取文件真实大小
struct stat st;
::fstat(fd, &st);
char *mmap_addr = (char*):mmap(nullptr, st.st_size, PROT_READ,
MAP_SHARED, fd, 0);
if(mmap_addr == MAP_FAILED)
{
perror("mmap");
return 3;
}
std::cout << mmap_addr <<std::endl;
// 取消映射
::munmap(mmap_addr, st.st_size);
// 关闭文件
::close(fd);
return 0;
}
下面我们再看文件读取 ,代码逻辑和写入几乎一样。读取流程:依旧传入文件名,以只读方式 O_RDONLY 打开文件,不需要调整文件大小。然后通过 fstat 系统调用获取文件真实大小,fstat 可以通过文件描述符拿到文件属性结构体 stat,里面的 st_size 就是文件大小。
接着进行 mmap 映射:地址设为 NULL,长度填获取到的文件大小,权限只需要 PROT_READ,映射类型依旧用 MAP_SHARED,偏移量为 0。映射成功后,直接用虚拟地址打印文件内容即可。最后取消映射、关闭文件。
测试读取:直接运行读取程序,传入之前的 log.txt,就能打印出文件里的 26 个字母。因为我们之前用 ftruncate 扩容到 4096 字节,未使用的区域默认填充 \0,所以打印到字母 z 就会自动停止。如果我们手动用 echo 写入新内容,也能正常读取出来,这就是 mmap 文件读取映射。
极简模拟实现 malloc
接下来我们重点理解 flags 参数里的 MAP_PRIVATE 和 MAP_SHARED:
MAP_SHARED:共享映射,进程对映射区的修改会同步到底层文件,也能被其他映射同一文件的进程看到,主要用于文件映射和进程间通信。MAP_PRIVATE:私有映射,采用写时复制机制,修改只在当前进程内生效,不会写回文件,也不会被其他进程看到,相当于进程私有的内存空间。
这一点非常关键:使用 MAP_PRIVATE + 匿名映射,mmap 可以直接在内存中开辟一块私有空间,不需要依赖任何文件 ,功能和 malloc 完全一样。事实上,malloc 和 new 的底层,很多场景就是通过 mmap 实现的,另外动态库的加载,也是用 mmap 完成的。
基于这个原理,我们可以直接用 mmap 模拟实现一个极简版的 malloc,原理就是用私有匿名映射申请内存,用 munmap 释放内存。
Malloc.c
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
// 使用mmap分配内存
void* my_malloc(size_t size) {
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
return ptr;
}
// 使用munmap释放内存
void my_free(void* ptr, size_t size) {
if (munmap(ptr, size) == -1) {
perror("munmap");
exit(EXIT_FAILURE);
}
}
int main() {
size_t size = 1024; // 分配1KB内存
char* ptr = (char*)my_malloc(size);
// 使用分配的内存(这里只是简单地打印指针值)
printf("Allocated memory at address: %p\n", ptr);
memset(ptr, 'A', size);
// 在这里可以使用ptr指向的内存 ...
for(int i = 0; i < size; i++)
{
printf("%c ", ptr[i]);
sleep(1);
}
// 释放分配的内存
my_free(ptr, size);
return 0;
}
gdb 调试映射信息
bash
(gdb) info proc mapping
process 237158
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x555555554000 0x555555555000 0x1000 0x0 /home/whb/code/test/test/malloc/a.out
0x555555555000 0x555555556000 0x1000 0x1000 /home/whb/code/test/test/malloc/a.out
0x555555556000 0x555555557000 0x1000 0x2000 /home/whb/code/test/test/malloc/a.out
0x555555557000 0x555555558000 0x1000 0x2000 /home/whb/code/test/test/malloc/a.out
0x555555558000 0x555555559000 0x1000 0x3000 /home/whb/code/test/test/malloc/a.out
0x7ffff7cd0000 0x7ffff7def000 0x22000 0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7def000 0x7ffff7f67000 0x178000 0x22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7f67000 0x7ffff7fb5000 0x4e000 0x19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fb5000 0x7ffff7fb9000 0x4000 0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fb9000 0x7ffff7fbb000 0x2000 0x1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fbb000 0x7ffff7fbc000 0x1000 0x0
0x7ffff7fbc000 0x7ffff7fc0000 0x4000 0x0 [vvar]
0x7ffff7fc0000 0x7ffff7fc2000 0x2000 0x0 [vdso]
0x7ffff7fc2000 0x7ffff7fd0000 0xe000 0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 0x23000 0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 0x8000 0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffb000 0x7ffff7ffc000 0x1000 0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x2e000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7ffff7fff000 0x7ffff8000000 0x1000 0x0 [stack]
0x7fffffff60000 0xffffffff601000 0x1000 0x0 [vsyscall]
(gdb)n
printf("Allocated memory at address: %p\n", ptr);
(gdb) info proc mapping
process 237158
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x555555554000 0x555555555000 0x1000 0x0 /home/whb/code/test/test/malloc/a.out
0x555555555000 0x555555556000 0x1000 0x1000 /home/whb/code/test/test/malloc/a.out
0x555555556000 0x555555557000 0x1000 0x2000 /home/whb/code/test/test/malloc/a.out
0x555555557000 0x555555558000 0x1000 0x2000 /home/whb/code/test/test/malloc/a.out
0x555555558000 0x555555559000 0x1000 0x3000 /home/whb/code/test/test/malloc/a.out
0x7ffff7cd0000 0x7ffff7def000 0x22000 0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7def000 0x7ffff7f67000 0x178000 0x22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7f67000 0x7ffff7fb5000 0x4e000 0x19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fb5000 0x7ffff7fb9000 0x4000 0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fb9000 0x7ffff7fbb000 0x2000 0x1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fbb000 0x7ffff7fbc000 0x1000 0x0
0x7ffff7fbc000 0x7ffff7fc0000 0x4000 0x0 [vvar]
0x7ffff7fc0000 0x7ffff7fc2000 0x2000 0x0 [vdso]
0x7ffff7fc2000 0x7ffff7fd0000 0xe000 0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 0x23000 0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 0x8000 0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffb000 0x7ffff7ffc000 0x1000 0x0 #mmap要求4KB对齐
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x2e000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fff000 0x7ffff8000000 0x1000 0x0 [stack]
0x7fffffff60000 0xffffffff601000 0x1000 0x0 [vsyscall]
(gdb)n
Allocated memory at address: 0x7ffff7ffb000
如果我们只想映射一块私有的内存空间,和文件没有任何关系 ,就可以使用匿名映射 。这种内存页不需要名称,直接用 MAP_PRIVATE 与 MAP_ANONYMOUS 组合进行映射。因为和文件无关,文件描述符 fd 直接设为 -1,告诉操作系统在物理内存中开辟空间,并映射到进程地址空间。映射成功后,就能得到一块完全属于当前进程的内存。
我们可以基于这个原理,用 mmap 模拟实现一个极简的 malloc。首先包含所需头文件,然后实现两个函数:my_malloc 用于申请内存,my_free 用于释放内存。
在主函数中做测试:调用 my_malloc(1024) 申请 1KB 内存,判断是否申请成功。成功后,把整块内存填充为字符 'A',每隔一秒打印一个字符,模拟内存使用。使用完毕后,调用 my_free 释放内存。
编译运行程序,会看到每隔一秒输出一个 'A',证明内存申请和使用都正常。
我们可以用 gdb 调试,直观看到内存映射的变化。编译时加上 -g 选项,启动 gdb 后,在 my_malloc 调用处打断点。运行到断点处时,使用 info proc mapping 命令查看进程的内存映射。此时还没有执行匿名映射,映射列表里没有我们申请的内存。
继续单步执行,完成 my_malloc 后,再次查看映射列表,会多出来一条新的映射记录 。这条记录的起始地址就是我们申请到的内存地址,大小是 0x1000(4096 字节)。即使我们只申请 1024 字节,系统也会按页对齐,分配 4KB 空间,这就是内存映射的规则。
这条映射记录后面没有对应的文件名,这就是匿名映射。而之前的文件映射,在这个位置会显示被映射的文件名称,这是匿名映射和文件映射最直观的区别。
所以我们平时使用的 malloc,底层很多时候就是用 mmap 匿名映射实现的,只不过 C 语言标准库做了更完善的内存管理。我们只需要知道:用 mmap 也能直接申请内存,不一定非要用 malloc。
最后我们再从内核层面理解 mmap 映射:每个进程都有独立的虚拟地址空间,由 struct vm_area_struct 管理每一段虚拟内存区域,记录区域的开始和结束地址。做文件映射 时,这个结构体里的指针会指向被打开的文件结构体,把虚拟地址空间和文件内核缓冲区关联起来,建立虚拟地址到物理内存的映射。做匿名映射时,这个文件指针置为空,操作系统直接分配物理内存,原理和 System V 共享内存一致。
这就是 mmap 文件映射与匿名映射的底层原理,也是我们本次学习的核心内容。
注意:
MAP_ANONYMOUS是一个用于mmap系统调用的标志,它指定要创建一个匿名内存映射,即这段内存没有文件作为其后端存储。这种类型的映射通常用于需要分配私有内存的场景,使用MAP_ANONYMOUS标志时,mmap会直接分配一块不与文件相关联的内存,例如进程内部的内存分配。