第六十章: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 了——有地址、有权限、有位置。它只是在等待第一次被触碰的那一刻,才会从虚空中诞生出真实的物理页。
"先画蓝图,后施工。"他轻声重复了一遍,这次他真正理解了这句话的含义。
/*
* 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");#include <stdio.h>
/*
* 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
int main() {
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");
return 0;
}道藏笔记
内核启示
是个多面手——匿名映射分配内存,文件映射把文件搬进地址空间,共享内存让多个进程看到同一块数据。内核实现 的过程很干净:先在虚拟地址空间里找一段够大的空地,创建新的 VMA 设好标志和操作函数,然后把地址返回给进程。注意——到这一步为止,没有分配任何物理页。
这就是延迟分配(demand paging)的精髓: 只画蓝图,不施工。进程拿到的只是一个虚拟地址——一块空地。只有当进程第一次踩上去,page fault 触发,内核才分配物理页、更新页表。这样做的好处是,一个进程 了 1GB 内存但只用了 4KB,系统不会浪费 99.96% 的物理页。 是"先画蓝图,后施工"——延迟分配的典型应用。
mmap 之试
进程想把文件或匿名区域映入自己的虚拟地址空间,本章对应的系统调用是什么?