上一章实现了 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 = &current->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 机制。