我们先明确两个目标一是理解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 才会比传统文件操作更快、效率更高我们看看具体的系统调用长什么样子// 头文件使用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.hmmap 有多个参数后面会逐一展开。它的核心作用就是把一个已经打开的文件映射到进程的地址空间中。基本说明允许用户空间程序将文件或设备的内容直接映射到进程的虚拟地址空间中。通过 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等可能也存在于某些系统上用于创建不与文件关联的匿名映射。/* * 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一起定义了映射区域在文件中的位置和大小。返回值/* * 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 代码写入映射#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 编译指定 C11 标准生成可执行程序。运行时必须传入文件名比如./a.out log.txt。运行后当前目录会生成一个log.txt大小正好是 4096 字节。我们用cat命令查看文件内容会发现内容每隔一秒不断增加这就是 mmap 直接写入的效果 —— 数据先写到内存映射区系统定期刷新到磁盘文件。程序跑完后文件里会写入 26 个英文字母虽然我们只写了 26 字节但文件大小依旧是 4096 字节后面未覆盖的区域保持默认值这就是完整的文件映射写入过程。基于这个原理即使是 10G 的大文件我们也可以用 mmap 映射到内存然后拆分成 1000 个小文件分别管理把大文件管理转化为内存块管理这就是 mmap 的强大之处。读取映射#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_SHAREDMAP_SHARED共享映射进程对映射区的修改会同步到底层文件也能被其他映射同一文件的进程看到主要用于文件映射和进程间通信。MAP_PRIVATE私有映射采用写时复制机制修改只在当前进程内生效不会写回文件也不会被其他进程看到相当于进程私有的内存空间。这一点非常关键使用MAP_PRIVATE 匿名映射mmap 可以直接在内存中开辟一块私有空间不需要依赖任何文件功能和malloc完全一样。事实上malloc和new的底层很多场景就是通过mmap实现的另外动态库的加载也是用 mmap 完成的。基于这个原理我们可以直接用 mmap模拟实现一个极简版的 malloc原理就是用私有匿名映射申请内存用 munmap 释放内存。Malloc.c#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 调试映射信息(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后再次查看映射列表会多出来一条新的映射记录。这条记录的起始地址就是我们申请到的内存地址大小是 0x10004096 字节。即使我们只申请 1024 字节系统也会按页对齐分配 4KB 空间这就是内存映射的规则。这条映射记录后面没有对应的文件名这就是匿名映射。而之前的文件映射在这个位置会显示被映射的文件名称这是匿名映射和文件映射最直观的区别。所以我们平时使用的malloc底层很多时候就是用mmap匿名映射实现的只不过 C 语言标准库做了更完善的内存管理。我们只需要知道用 mmap 也能直接申请内存不一定非要用 malloc。最后我们再从内核层面理解 mmap 映射每个进程都有独立的虚拟地址空间由struct vm_area_struct管理每一段虚拟内存区域记录区域的开始和结束地址。做文件映射时这个结构体里的指针会指向被打开的文件结构体把虚拟地址空间和文件内核缓冲区关联起来建立虚拟地址到物理内存的映射。做匿名映射时这个文件指针置为空操作系统直接分配物理内存原理和 System V 共享内存一致。这就是 mmap 文件映射与匿名映射的底层原理也是我们本次学习的核心内容。注意MAP_ANONYMOUS是一个用于mmap系统调用的标志它指定要创建一个匿名内存映射即这段内存没有文件作为其后端存储。这种类型的映射通常用于需要分配私有内存的场景使用MAP_ANONYMOUS标志时mmap会直接分配一块不与文件相关联的内存例如进程内部的内存分配。