上一章实现了 TCP 服务端,这章来实现一个经典的内存管理功能:mmap 文件映射。
什么是 mmap 文件映射
mmap 把文件的一段内容映射到进程的虚拟地址空间。映射后可以像读内存一样读文件——不需要 read() 系统调用,直接解引用指针。
int fd = open("/bin/busybox", O_RDONLY);
char *p = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
// p[0..3] 就是文件的前 4 字节(ELF magic: 7f 45 4c 46)
关键设计:VMA 和 Lazy 分配
mmap 不会立刻分配物理内存,而是登记一个 VMA(虚拟内存区域),等到进程真正访问这段地址时,才通过缺页异常按需读取文件内容并建立页表映射。
mmap(fd) → 注册 VMA(vaddr, len, inum, offset) → 返回虚拟地址
↓ 进程访问该地址
#PF not-present → 找到 VMA → 分配物理页
→ ext2_read(inum, offset, page)
→ 建立页表 → 重试指令
这就是操作系统课上说的"按需分页"(demand paging)。
实现步骤
1. 定义 VMA 结构
在 process_t 中加入 VMA 数组:
typedef struct {
uint64_t vaddr;
uint64_t len;
uint32_t inum; // ext2 inode 号
uint32_t file_offset;
int flags;
} vma_t;
2. mmap 系统调用:只登记,不分配
mmap 的 Linux x86-64 ABI 有 6 个参数(addr, len, prot, flags, fd, offset),而我们的 syscall_handler 只接收 5 个。offset(r9)被压在 syscall_frame[0] 上,直接读取:
uint64_t mmap_offset = syscall_frame ? syscall_frame[0] : 0;
// 找到文件 inum,登记 VMA,返回虚拟地址(不分配物理页)
3. 缺页处理:读文件内容填充页面
在 isr.c 的缺页处理中,识别"not-present + 在某个 VMA 内"的情况:
if (!is_present && current) {
uint64_t fault_page = cr2 & ~0xFFFULL;
for (int vi = 0; vi < current->vma_count; vi++) {
vma_t *v = ¤t->vmas[vi];
if (fault_page >= v->vaddr && fault_page < v->vaddr + v->len) {
uint64_t phys = pmm_alloc();
memset(phys, 0, 4096);
ext2_read(v->inum, v->file_offset + (fault_page - v->vaddr), phys, 4096);
vmm_map_page(current->pml4, fault_page, phys, PAGE_USER_RW);
return; // 成功,CPU 重新执行触发 fault 的指令
}
}
}
验证
mmap_test 程序 mmap /bin/busybox 的前 4096 字节,读取 ELF 魔数:
mmap ok, first bytes: 7f 45 4c 46
ELF magic ok!
7f 45 4c 46 = \x7fELF,正是 ELF 文件头魔数。缺页处理正确从 ext2 读取了文件内容。
小结
| 组件 | 改动 |
|---|---|
process.h |
添加 vma_t 结构和 vmas[] 数组 |
vfs.c |
添加 vfs_get_inum() 获取 inode 号 |
syscall.c |
SYS_MMAP 支持 fd≥0 的文件映射 |
isr.c |
缺页处理扩展:not-present 查 VMA 并按需加载 |
mmap 是现代 OS 的核心特性,动态链接库(.so)加载、进程 exec(代码段)、文件数据库等都依赖它。这一章建立了最基础的 demand paging 机制。