函数选择
我们逐行分析以下三个关键函数:
kvminit():初始化内核页表kvmmake():创建并映射内核页表mappages():完成虚拟地址到物理地址的映射
kvminit() – 初始化内核页表
位置:kernel/vm.c
void kvminit(void)
{
kernel_pagetable = kvmmake(); // 创建页表结构
}
- 全局变量
kernel_pagetable是一个内核页表指针(pagetable_t类型,本质是uint64*)。 - 实际工作都由
kvmmake()完成。
kvmmake() – 创建页表并设置内核映射
pagetable_t
kvmmake(void)
{
pagetable_t pagetable;
pagetable = (pagetable_t) kalloc(); // 分配一个物理页作为 root 页表
memset(pagetable, 0, PGSIZE); // 清零
// 映射 trampoline(用于 trap/ret)
kvmmap(pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
// 映射内核 text 段(只读 + 可执行)
kvmmap(pagetable, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);
// 映射内核 data 段(只读 + 可写)
kvmmap(pagetable, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);
// 映射设备内存(UART、PLIC、VIRTIO 等)
kvmmap(pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
kvmmap(pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
kvmmap(pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
return pagetable;
}
分析:
kalloc()分配的页是物理地址,但 xv6 中通过 direct mapping,可以直接作为虚拟地址用;-
每次
kvmmap()调用的参数:- 第一个是目标页表;
- 第二个是虚拟地址(VA);
- 第三个是对应的物理地址(PA);
PGSIZE是页大小(通常 4096);- 最后是权限位(读写执行)。
kvmmap() 和 mappages()
void
kvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
if (mappages(pagetable, va, pa, sz, perm) != 0)
panic("kvmmap");
}
kvmmap() 就是一层封装,调用 mappages() 处理映射。
重点函数:mappages()
int
mappages(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + sz - 1);
for (;;){
pte = walk(pagetable, a, 1); // 取出/创建 PTE 指针
if (pte == 0)
return -1;
if (*pte & PTE_V) // 已经映射了
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V; // 写入物理页号 + 权限 + valid 位
if (a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
理解:
- 对
va到va + sz范围内每一页做一次映射; walk()负责找到对应的三级页表项,如果需要就创建;PA2PTE(pa)把物理地址变成 PFN(右移 12 位);| perm | PTE_V把权限位与 valid 位合并。
walk() – 模拟 Sv39 页表查找过程
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
for (int level = 2; level > 0; level--) {
pte_t *pte = &pagetable[PX(level, va)];
if (*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
} else {
if (!alloc || (pagetable = (pde_t*)kalloc()) == 0)
return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE((uint64)pagetable) | PTE_V;
}
}
return &pagetable[PX(0, va)];
}
解释:
- Sv39 使用三级页表,每级 9 bits;
PX(level, va)宏会取出第level级页表索引(level=2 是最高级);walk()遍历每一级,如果没有页表页,就在alloc==1时新建一个;- 最终返回最底层页表(level 0)的条目的地址。
小结:完整地址空间建立过程
main() -> kvminit()
-> kvmmake()
-> kalloc() // 分配 root 页表
-> kvmmap() * N次 // 映射 trampoline、text、data、设备等
-> mappages() // 为每页创建映射
-> walk() // 遍历多级页表,找到 PTE
-> kalloc() // 必要时创建中间页表页
每个 PTE 保存了:
- PFN(物理页号)
- 权限标志位(R/W/X)
PTE_V:有效位