部分前置知识:
首先是物理地址布局理解
物理地址图
──────────────────────────────────────────────────────────────
地址范围 | 描述
──────────────────────────────────────────────────────────────
0x00001000 - 0x0000FFFF | 启动ROM (由QEMU提供)
──────────────────────────────────────────────────────────────
0x02000000 - 0x0200FFFF | CLINT (核心本地中断器)
──────────────────────────────────────────────────────────────
0x0C000000 - 0x0CFFFFFF | PLIC (平台级中断控制器)
──────────────────────────────────────────────────────────────
0x10000000 - 0x10000FFF | UART0 (通用异步收发传输器0)
──────────────────────────────────────────────────────────────
0x10001000 - 0x10001FFF | Virtio磁盘
──────────────────────────────────────────────────────────────
0x80000000 - 0x8000FFFF | 启动ROM在机器模式下跳转到这里
| 包含 entry.S, 然后是内核的代码和数据
──────────────────────────────────────────────────────────────
0x80010000 - 0x8FFFFFFF | 内核使用的内存区,内核页面分配区开始
──────────────────────────────────────────────────────────────
0x90000000 - 0x97FFFFFF | 未使用的RAM (假设系统有128MB RAM)
──────────────────────────────────────────────────────────────
0x98000000 - 0xFFFFFFFF | (可能用于扩展的内存或未映射)
──────────────────────────────────────────────────────────────
内核地址映射
──────────────────────────────────────────────────────────────
虚拟地址 | 物理地址
──────────────────────────────────────────────────────────────
KERNBASE (0x80000000) | 0x80000000
PHYSTOP (0x88000000) | 0x88000000 (KERNBASE + 128MB)
──────────────────────────────────────────────────────────────
特殊地址映射
──────────────────────────────────────────────────────────────
虚拟地址 | 描述
──────────────────────────────────────────────────────────────
TRAMPOLINE (MAXVA - PGSIZE) | 映射到最高地址页
TRAPFRAME (TRAMPOLINE - PGSIZE) | 用户态和内核态切换使用的trapframe页
KSTACK(p) | (TRAMPOLINE - ((p)+1)* 2*PGSIZE) 映射到trampoline之下的内核栈
──────────────────────────────────────────────────────────────
pagetable_t是uint64*页表指针(64位无符号整数),他指向包含512个页表项的数组,每个页表项也是uint64类型即64位无符号整数,512×64 bits=512×8 bytes=4096 bytes=4 KB,那么每个页表就是占用4KB物理内存
typedef uint64 pte_t;
typedef uint64 *pagetable_t; // 512 PTEs
申请内存(kalloc.c)
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void *
kalloc(void)
{
struct run *r;//空闲链表指针
acquire(&kmem.lock);//获取自旋锁
r = kmem.freelist;//获取空闲内存链表第一个内存节点
if(r)
kmem.freelist = r->next;//如果获取了,那么后移链表首位,就不会重复分配内存
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // 初始化为5,填充"垃圾值"
return (void*)r;
}
添加页表映射
void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)//进行页表映射
panic("kvmmap");//失败就打印错误信息,并停止操作系统其他操作
}
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)//查找或者创建页表项
return -1;
if(*pte & PTE_V)
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V;
//首先PA2PTE(pa)转化为页表项格式,然后再与标志位和权限进行与运算
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 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)];//返回虚拟地址页表项地址
}
copyin函数,将用户态下的地址通过页表查询,转换成物理地址,再把数据从用户空间,拷贝到内核空间,供内核使用。后面会修改成,不拷贝到内核空间,通过进程内核页表直接解引用用户指针。
int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len) {
uint64 n, va0, pa0;
while (len > 0) {
// 将 srcva 对齐到页面边界
va0 = PGROUNDDOWN(srcva);
// 获取 va0 对应的物理地址 pa0
pa0 = walkaddr(pagetable, va0);
// 如果获取失败,则返回错误
if (pa0 == 0)
return -1;
// 计算可以拷贝的字节数
n = PGSIZE - (srcva - va0);
// 如果计算出的字节数大于剩余需要拷贝的字节数,则只拷贝剩余的字节数
if (n > len)
n = len;
// 将数据从用户空间拷贝到内核空间
memmove(dst, (void *)(pa0 + (srcva - va0)), n);
// 更新剩余字节数、目标地址和源地址
len -= n;
dst += n;
srcva = va0 + PGSIZE;
}
return 0;
}
Print a page table
主要是递归地打印页表内容
void
_vmprint(pagetable_t pagetable,int level)
{
for(int i=0;i<512;++i)
{//遍历页表项
pte_t pte=pagetable[i];
if((pte & PTE_V)&&(PTE_R|PTE_W|PTE_X))
{//非叶子结点
for(int i=1;i<=level;++i)
{
printf("..");
if(i!=level)printf(" ");
}
uint64 child =PTE2PA(pte);//页表项指针的物理地址就是子页表项的地址
printf("%d: pte %p pa %p\n",i,pte,child);
if((pte & (PTE_R|PTE_W|PTE_X))==0)//如果没有读,写,执行,那就是叶子结点
_vmprint((pagetable_t)child,level+1);
}
}
}
void
vmprint(pagetable_t pagetable){
printf("page table %p\n", pagetable);
//解决递归避免重复打印的问题
_vmprint(pagetable, 1);
}
A kernel page table per process
Xv6有一个单独的用于在内核中执行程序时的内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x
映射到物理地址仍然是x
。Xv6还为每个进程的用户地址空间提供了一个单独的页表,只包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()
的缓冲区指针)时,内核必须首先将指针转换为物理地址。
本实验就是给每个进程添加内核页表的副本,然后每个进程都可以切换内核态时使用自己的页表,就可以直接使用用户指针。
首先给kernel/proc.h里面的struct proc
加上内核页表的字段。
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
pagetable_t kernelpt; // 进程的内核页表
struct trapframe *trapframe; // data page for trampoline.S
在vm.c
中添加新的方法proc_kpt_init
,该方法用于在allocproc
中初始化进程的内核页表。这个函数还需要一个辅助函数uvmmap
,该函数和kvmmap
方法几乎一致,不同的是kvmmap
是对Xv6的内核页表进行映射,而uvmmap
将用于进程的内核页表进行映射。
void//类似kvmmap
uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(pagetable, va, sz, pa, perm) != 0)
panic("uvmmap");
}
//初始化内核页表副本
pagetable_t
proc_kpt_init(){
pagetable_t kernelpt = uvmcreate();
if (kernelpt == 0) return 0;
uvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
uvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
uvmmap(kernelpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
uvmmap(kernelpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
uvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
uvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
uvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return kernelpt;
}
然后在kernel/proc.c里面的allocproc
调用
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
//初始化进程的内核页表
p->kernelpt = proc_kpt_init();
if(p->kernelpt == 0){//如果分配失败
freeproc(p);
release(&p->lock);
return 0;
}
根据提示,为了确保每一个进程的内核页表都关于该进程的内核栈有一个映射。我们需要将procinit
方法中相关的代码迁移到allocproc
方法中。很明显就是下面这段代码,将其剪切到上述内核页表初始化的代码后。
//为内核栈申请一块区域
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;//进程的kstack指针指向虚拟地址
我们需要修改scheduler() (进程调度函数)
来加载进程的内核页表到SATP寄存器。提示里面请求阅读kvminithart()
。kvminithart
是用于原先的内核页表,我们将进程的内核页表传进去就可以。在vm.c里面添加一个新方法proc_inithart
。
void
proc_inithart(pagetable_t kpt){
w_satp(MAKE_SATP(kpt));
sfence_vma();
}
在scheduler()
内调用即可,但在结束的时候,需要切换回原先的kernel_pagetable
。直接调用调用上面的kvminithart()
就能把Xv6的内核页表加载回去。
// SATP使用内核也表
proc_inithart(p->kernelpt);
swtch(&c->context, &p->context);
//切换回全局内核页表
kvminithart();
在freeproc
中释放一个进程的内核页表。首先释放页表内的内核栈,调用uvmunmap
可以解除映射,最后的一个参数(do_free
)为一的时候,会释放实际内存。
//取消内核页表映射,释放页表的内核栈
uvmunmap(p->kernelpt, p->kstack, 1, 1);
p->kstack = 0;
然后释放进程的内核页表,先在kernel/proc.c里面添加一个方法proc_freekernelpt
。如下,历遍整个内核页表,然后将所有有效的页表项清空为零。如果这个页表项不在最后一层的页表上,需要继续进行递归。
void
proc_freekernelpt(pagetable_t kernelpt)
{
for(int i = 0; i < 512; i++){
pte_t pte = kernelpt[i];
if(pte & PTE_V){
kernelpt[i] = 0;//将页表项置为0,取消映射
if ((pte & (PTE_R|PTE_W|PTE_X)) == 0){
uint64 child = PTE2PA(pte);
proc_freekernelpt((pagetable_t)child);
}//递归释放子页表
}
}
kfree((void*)kernelpt);// 释放内核页表所占用的内存
}
修改vm.c
中的kvmpa
,将原先的kernel_pagetable
改成myproc()->kernelpt
,使用进程的内核页表进行虚拟地址和物理地址的转换。
uint64
kvmpa(uint64 va)
{//映射物理地址
uint64 off = va % PGSIZE;
pte_t *pte;
uint64 pa;
pte = walk(myproc()->kernelpt, va, 0);//这里传入内核页表
if(pte == 0)
panic("kvmpa");
if((*pte & PTE_V) == 0)
panic("kvmpa");
pa = PTE2PA(*pte);
return pa+off;
}
Simplify copyin/copyinstr
本实验是实现将用户空间的映射添加到每个进程的内核页表,将进程的页表复制一份到进程的内核页表就好。
先实现一个复制page table的函数u2kvmcopy
来将user page table复制到process kernel page table,注意在复制的过程中需要清除原先PTE中的PTE_U标志位,否则kernel无法访问。
区别:
copyin在用户进程使用,va != pa,copyin_new在kernel中使用,va = pa。
void
u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz) {
pte_t *pte_from, *pte_to;
// 将 oldsz 对齐到页面大小
oldsz = PGROUNDUP(oldsz);
// 遍历从 oldsz 到 newsz 之间的每个页面
for (uint64 i = oldsz; i < newsz; i += PGSIZE) {
// 获取用户页表中虚拟地址 i 对应的页表项
if ((pte_from = walk(pagetable, i, 0)) == 0)
panic("u2kvmcopy: src pte does not exist");
// 在内核页表中为虚拟地址 i 创建对应的页表项
if ((pte_to = walk(kernelpt, i, 1)) == 0)
panic("u2kvmcopy: pte walk failed");
// 获取用户页表项中对应的物理地址
uint64 pa = PTE2PA(*pte_from);
// 获取用户页表项的标志位,并去掉用户态标志 PTE_U
uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);
// 将物理地址和标志位写入内核页表项
*pte_to = PA2PTE(pa) | flags;
}
}
然后再fork,exec,sbrk函数中调用此函数,还有在kernel/vm.c中的copyin
的主题内容替换为对copyin_new
的调用。