Skip to content

第六十章:mmap 之术

元婴初期

涉及内核源码:

林小源在宫殿中闲逛时,看到了一件奇怪的事。

一块崭新的大陆凭空出现在宫殿的边缘。没有预兆,没有声响——它就那样静静地浮在那里,表面什么都没有,一片空白。

"那是什么?"林小源指着新大陆问。

"。"mm_struct 说,"有人调用了 系统调用,要了一块新的虚拟地址空间。"

林小源走近那块空白大陆。它的边界清晰, 已经刻好了,flags 也设好了——,匿名映射。但地底下什么都没有——页表冰层里对应的区域是空的,没有 PTE,没有物理页。

"等等。"林小源蹲下来敲了敲地面,发出空洞的回响,"这块地没有地基?"

"没有。"mm_struct 的声音平静得像在说天气," 只创建 VMA,不分配物理页。这就是延迟分配——demand paging。"

林小源站起来,困惑地看着这块悬浮大陆。它有边界,有规矩,但没有实质。

"那它有什么用?"

"用处大了。"mm_struct 说,"你想想——一个进程 了 1GB 的内存,但它实际只用了 4KB。如果 的时候就分配 1GB 的物理页,那 99.96% 的物理页都浪费了。延迟分配让内核只分配实际使用的部分。"

"可是——"林小源犹豫了一下,"进程访问这块空地的时候怎么办?"

"page fault。"mm_struct 说,"进程第一次访问这个虚拟地址时,页表里没有对应的 PTE,CPU 触发 page fault。内核接管控制权,发现这个地址在合法的 VMA 里,就分配一个物理页,更新页表,然后让进程继续执行。整个过程——对进程来说几乎无感。"

林小源想了想,觉得这个设计确实巧妙。先画蓝图,后施工。蓝图不占材料,只有真正需要住人的时候,才开始盖房子。

林小源对 的好奇越来越深。他开始在宫殿中寻找更多线索。

"mmap 不只能分配匿名内存吧?"他问。

"当然不。"mm_struct 的声音带了一丝教学的意味,"mmap 有三种用法。"

它让林小源看宫殿中的另一个角落。那里有一块大陆,表面刻着一个文件名——某个 .so 动态链接库的路径。这块大陆的 PTE 不指向普通物理页,而是指向文件缓存。

"文件映射。"mm_struct 说,"把一个文件映射到进程的地址空间。访问这段虚拟地址时,内核自动从文件中读取数据。不需要 read() 系统调用——直接像访问内存一样读文件。"

林小源又看到另一块大陆。这块大陆的 flags 里有 标志。

"共享映射。"mm_struct 说,"多个进程可以映射同一个文件或同一块匿名内存。一个进程写入的内容,其他进程能立刻看到。进程间通信的一种方式。"

"还有私有映射。"林小源补充,他已经看到了另一块只有 标志的大陆。

"对。私有映射用写时复制——COW。"mm_struct 说,"多个进程可以映射同一个文件,但每个进程的修改是私有的,不会影响其他进程,也不会写回文件。"

林小源在宫殿中走了一圈,发现 的痕迹无处不在——动态链接库、Java 的内存映射文件、数据库的存储引擎——到处都是 创建的 VMA。

"mmap 不是一种分配方式——它是一整套基础设施。"他喃喃自语,忽然觉得自己好像抓住了什么。

林小源回到那块最初让他好奇的空白大陆前。

"do_mmap 的过程是什么?"他问。

mm_struct 的声音变得正式,像在讲解一项精密的仪式:"当进程调用 时,内核执行 。第一步——在宫殿中找到一块够大的空地。不能和现有的 VMA 重叠。"

林小源看到宫殿边缘的虚空中,一条扫描线在移动,寻找合适的位置。

"第二步——创建新的 VMA。设好 。如果有文件关联,设好 。"

一块新的大陆在扫描线停下的地方浮现出来,上面的数字和标志逐一亮起。

"第三步——把 VMA 插入红黑树和链表。"

大陆沉入地下,与红黑树的根系连接在一起。同时,链表的街道也把它串了进去。

"第四步——返回虚拟地址。"

大陆的 地址化作一道光,飞向远方——那是返回给进程的值。

"整个过程,"mm_struct 说,"没有分配任何物理页。进程拿到的只是一个地址——一块空地。真正的施工,要等到进程第一次踩上去的那一刻。"

林小源看着那块空白大陆,心中涌起一种奇异的感觉。它看起来什么都没有,但它已经是一个合法的 VMA 了——有地址、有权限、有位置。它只是在等待第一次被触碰的那一刻,才会从虚空中诞生出真实的物理页。

"先画蓝图,后施工。"他轻声重复了一遍,这次他真正理解了这句话的含义。


c
/*
 * mmap() 系统调用:
 *
 * void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
 *
 * 参数:
 *   addr    — 建议的映射地址(通常为 NULL,由内核选择)
 *   length  — 映射长度
 *   prot    — 保护标志(PROT_READ, PROT_WRITE, PROT_EXEC)
 *   flags   — 映射标志(MAP_PRIVATE, MAP_SHARED, MAP_ANONYMOUS)
 *   fd      — 文件描述符(匿名映射时为 -1)
 *   offset  — 文件偏移
 *
 * 返回值:映射的虚拟地址,失败返回 MAP_FAILED
 *
 * 映射类型:
 *   MAP_PRIVATE   — 私有映射(写时复制)
 *   MAP_SHARED    — 共享映射(修改写回文件)
 *   MAP_ANONYMOUS — 匿名映射(无文件关联)
 *   MAP_FIXED     — 固定地址映射
 */

#define PROT_READ   0x1
#define PROT_WRITE  0x2
#define PROT_EXEC   0x4

#define MAP_PRIVATE   0x02
#define MAP_SHARED    0x01
#define MAP_ANONYMOUS 0x20
#define MAP_FIXED     0x10

printf("=== mmap — 内存映射之术 ===\n\n");

printf("--- 匿名映射 ---\n");
printf("mmap(NULL, 4096, PROT_READ | PROT_WRITE,\n");
printf("     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)\n");
printf("  → 分配 4KB 匿名内存\n");
printf("  → 不关联任何文件\n");
printf("  → 延迟分配(demand paging)\n\n");

printf("--- 文件映射 ---\n");
printf("mmap(NULL, file_size, PROT_READ,\n");
printf("     MAP_PRIVATE, fd, 0)\n");
printf("  → 把文件映射到内存\n");
printf("  → 可以像访问内存一样读取文件\n");
printf("  → 私有映射,修改不写回文件\n\n");

printf("--- 共享映射 ---\n");
printf("mmap(NULL, 4096, PROT_READ | PROT_WRITE,\n");
printf("     MAP_SHARED | MAP_ANONYMOUS, -1, 0)\n");
printf("  → 匿名共享内存\n");
printf("  → 多个进程可以共享\n");
printf("  → 类似 shmget/shmat\n\n");

printf("--- mmap 的内核实现 ---\n");
printf("do_mmap():\n");
printf("  1. 查找合适的虚拟地址范围\n");
printf("  2. 创建新的 VMA\n");
printf("  3. 设置 VMA 的标志和操作函数\n");
printf("  4. 返回虚拟地址\n");
printf("  注意:此时不分配物理页!\n");

printf("\n--- 延迟分配 (Demand Paging) ---\n");
printf("mmap 只是创建 VMA,不分配物理页\n");
printf("当进程首次访问页面时:\n");
printf("  1. 触发页 fault\n");
printf("  2. 内核分配物理页\n");
printf("  3. 更新页表\n");
printf("  4. 进程继续执行\n\n");
printf("好处:减少不必要的内存分配\n");

道藏笔记

内核启示

是个多面手——匿名映射分配内存,文件映射把文件搬进地址空间,共享内存让多个进程看到同一块数据。内核实现 的过程很干净:先在虚拟地址空间里找一段够大的空地,创建新的 VMA 设好标志和操作函数,然后把地址返回给进程。注意——到这一步为止,没有分配任何物理页。

这就是延迟分配(demand paging)的精髓: 只画蓝图,不施工。进程拿到的只是一个虚拟地址——一块空地。只有当进程第一次踩上去,page fault 触发,内核才分配物理页、更新页表。这样做的好处是,一个进程 了 1GB 内存但只用了 4KB,系统不会浪费 99.96% 的物理页。 是"先画蓝图,后施工"——延迟分配的典型应用。


破关试炼

mmap 之试

进程想把文件或匿名区域映入自己的虚拟地址空间,本章对应的系统调用是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道