阅读材料
Xv6代码:memlayout.h、vm.c
教材:Chapter 3 3.1 - 3.3
内核虚拟地址空间初始化
之前的文章提到了,在main()函数中,内核会指派CPU0来完成绝大部分的初始化工作,其中就包括内核的虚拟地址空间初始化工作,而内核通过调用kvminit()函数来实现内核虚拟地址空间的构建。
kvminit函数
该函数是一个包装函数,通过调用kvmmake函数来初始化kernel_pagetable。该变量是一个全局变量,记录了内核根页表的基地址。
// Initialize the one kernel_pagetable
void kvminit(void)
{
kernel_pagetable = kvmmake();
}
kvmmake函数
这个函数才是真正负责构建内核虚拟地址空间的函数。该函数通过调用kvmmap函数来实现虚拟地址到物理地址映射的构建。
需要注意的是:
- 内核虚拟地址空间采用直接映射方式。即对于内核来说,虚拟地址等于物理地址,可以参考上图中的虚线
- trampoline页表映射了两次。该页表在物理地址空间中只存在一份,紧跟在Kernel text之后。而在虚拟地址空间中,该页第一次通过直接映射,映射到了对应的虚拟地址上;第二次通过手动指定,映射到了宏MAXVA指定的地址下紧挨的一页。
函数调用链:
kvminit()--->kvmmake()--->kvmmap()--->mappages()--->walk()
pagetable_t kvmmake(void)
{
pagetable_t kpgtbl;
kpgtbl = (pagetable_t)kalloc();
memset(kpgtbl, 0, PGSIZE);
// uart registers
kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// PLIC
kvmmap(kpgtbl, PLIC, PLIC, 0x4000000, PTE_R | PTE_W);
// map kernel text executable and read-only.
kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
// allocate and map a kernel stack for each process.
proc_mapstacks(kpgtbl);
return kpgtbl;
}
kvmmap函数
该函数是一个包装函数,用于将物理地址空间映射到虚拟地址空间。通过调用mappages函数来实现其功能。
- kptbl:根页表的首地址
- va:要映射的虚拟地址
- pa:要映射的物理地址
- sz:要映射的内存区域大小
- perm:页表项描述符
void kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
{
if (mappages(kpgtbl, va, sz, pa, perm) != 0)
panic("kvmmap");
}
proc_mapstacks函数
该函数为每一个进程(进程总数量用宏NPROC定义)都在内核地址空间中分配了2个页的空间。第一个页是保护页,用于防止进程栈溢出;第二个页是进程的kstack页。
这些页从trampoline页起始地址开始,从高地址往低地址增长,从kstack0、kstack1、…… kstack63
void proc_mapstacks(pagetable_t kpgtbl)
{
struct proc *p;
for (p = proc; p < &proc[NPROC]; p++)
{
char *pa = kalloc();
if (pa == 0)
panic("kalloc");
uint64 va = KSTACK((int)(p - proc));
kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
}
}
mappages函数
该函数通过调用walk函数,为指定的虚拟内存逐级分配页表,从而实现了虚拟内存到物理内存映射的创建
int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
if ((va % PGSIZE) != 0)
panic("mappages: va not aligned");
if ((size % PGSIZE) != 0)
panic("mappages: size not aligned");
if (size == 0)
panic("mappages: size");
a = va;
last = va + size - PGSIZE;
for (;;)
{
if ((pte = walk(pagetable, a, 1)) == 0)
return -1;
if (*pte & PTE_V)
panic("mappages: remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if (a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
walk函数
该函数用于遍历页表结构,找到最末尾一级的页表项
- 如果alloc == 0:如果该页表项有效则返回其物理地址,若无效则返回0
- 如果alloc == 1:如果该页表项有效则返回其物理地址,若无效则调用物理内存分配器kalloc函数分配一个物理页,并返回物理地址;如果kalloc函数返回0,说明物理内存已满,分配失败,返回0
pte_t* walk(pagetable_t pagetable, uint64 va, int alloc)
{
if (va >= MAXVA)
panic("walk");
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(pagetable) | PTE_V;
}
}
return &pagetable[PX(0, va)];
}
开启分页功能
在main函数中,所有核心(包括cpu0)都会通过调用kvminithart函数来开启分页功能。也就是说,在调用该函数前,内核访问的都是物理内存;调用该函数后,内核访问的就都是虚拟内存了
kvminithart函数
该函数通过将之前定义的全局变量kernel_pagetable写入到satp寄存器中来开启分页功能
void kvminithart()
{
// wait for any previous writes to the page table memory to finish.
sfence_vma();
w_satp(MAKE_SATP(kernel_pagetable));
// flush stale entries from the TLB.
sfence_vma();
}