xv6 在 创建地址空间(Creating an Address Space) 的过程,主要涉及两个方面:
- 内核页表的建立(kernel address space)
- 用户进程页表的建立(user address space)
这两个过程都是通过一系列函数配合完成的,核心在于如何构建多级页表、设置映射、以及最终启用分页机制。下面我将分步详细说明:
🧠 一、内核地址空间的创建
🔹 1. main() 中调用 kvminit()
位于 kernel/main.c,初始化内核页表。
kvminit(); // 创建页表
kvminithart(); // 启用页表
🔹 2. kvminit() → kvmmake()
pagetable_t kvmmake(void)
作用: 为内核创建页表,返回根页表地址。
主要步骤:
kalloc():分配一个物理页,作为 root page table;-
多次调用
kvmmap(),把下面内容映射到页表中:- 内核代码段(text, data)
- 所有物理内存([0, PHYSTOP])
- 外设内存地址空间(UART、PLIC 等 MMIO)
🔹 3. kvmmap() → mappages()
int mappages(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
作用: 为一段虚拟地址 [va, va+sz) 映射到物理地址 [pa, pa+sz)
- 对每个页大小(PGSIZE = 4096)调用一次
walk()查找对应 PTE; - 若对应页表项不存在,则
walk()中分配新的页表页(level 2/1); -
设置 PTE:
- 将物理地址的 PFN 放入 PTE;
- 设置权限标志位:PTE_R / PTE_W / PTE_X / PTE_V
🔹 4. walk() 函数
pte_t *walk(pagetable_t pagetable, uint64 va, int alloc)
作用: 模拟硬件多级页表遍历,返回某个虚拟地址对应的 PTE 地址。
- 使用 Sv39 模式,将虚拟地址拆成三级索引,每级 9 位;
- 如果中间页表页不存在(PTE_V == 0),并且
alloc == 1,就调用kalloc()分配; - 直接返回最后一层页表项地址。
🔹 5. kvminithart():启动分页
void kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable)); // 写入 root 页表 PFN 到 satp
sfence_vma(); // 刷新 TLB
}
这步执行后,分页开启,虚拟地址 → 物理地址 翻译生效。
🧠 二、用户地址空间的创建
用户进程的页表是每个进程私有的,由 proc_pagetable() 创建。
🔹 1. 在进程创建时调用 proc_pagetable()
pagetable_t proc_pagetable(struct proc *p)
步骤:
- 分配 root page table;
- 拷贝 trampoline 和 trapframe 映射;
- 不映射内核段!
🔹 2. 用户代码映射(比如 exec/load):
uvmalloc(pagetable, 0, sz, PTE_R|PTE_W|PTE_X)
为用户程序的代码段、数据段分配物理内存,并建立虚拟映射。
底层调用的是 mappages(),和内核页表一样,只是页表对象是用户的。
🔹 3. 用户页表激活
在 scheduler() 中运行进程前,使用 uvm_switch():
void uvm_switch(pagetable_t pagetable)
{
w_satp(MAKE_SATP(pagetable));
sfence_vma();
}
这使得 CPU 开始使用该用户页表。
🔄 页表更新与 TLB
任何对页表的改动(新增/删除 PTE),都要通过 sfence_vma() 来刷新 TLB,否则会使用旧缓存,可能导致严重安全漏洞(如访问别的进程的内存)。
✅ 总结:创建地址空间流程
| 阶段 | 关键函数 | 说明 |
|---|---|---|
| 创建内核页表 | kvminit() → kvmmake() → kvmmap() | 分配 root,映射内核地址空间 |
| 启用分页 | kvminithart() | 设置 satp,启用分页 |
| 创建用户页表 | proc_pagetable() | 用户进程私有页表 |
| 映射用户程序段 | uvmalloc() → mappages() | 分配内存 + 映射 |
| 切换页表 | uvm_switch() | 写入 satp,刷新 TLB |