【无标题】

lab3: pgtbl

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g1VAtxqH-1638793994235)(/Users/liuzhilei/workspace-git/study-repo/6.S081/lab/pgtbl.assets/image-20211206202845356.png)]

Print a page table(easy)

添加一个打印页表的内核函数,以如如下格式打印出传进的页表,用于后面两个实验调试用:

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

如下图所示,RISC-V 的逻辑地址寻址是采用三级页表的形式,9 bit 一级索引找到二级页表,9 bit 二级索引找到三级页表,9 bit 三级索引找到内存页,最低 12 bit 为页内偏移(即一个页 4096 bytes)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qBSb4aAE-1638793994243)(/Users/liuzhilei/workspace-git/study-repo/6.S081/lab/pgtbl.assets/image-20211204230201300.png)]

理解了页表的结构原理,该task就能够轻松解决;实际上我们只要遍历整个三级页表,打印出有效的页表项((*pte & PTE_V) != 0)即可,下面给出递归打印页表的实现:

// kernel/exec.c
void iPrintf(int i) {
  for (;i > 0;i--) printf(".. ");
  printf("..");
}

// 递归打印页表
// depth表示页表的深度,depth=0表示顶级页表
// curPt表示当前页表的基地址
void rPrintf(pagetable_t curPt, int depth) {
  if (depth > 2) return;
  for (int i = 0; i < 512; i++) {
    pte_t* pte = curPt + i;
    if ((*pte & PTE_V) == 0) continue;	// 跳过无效表项
    iPrintf(depth);
    printf("%d: pte %p pa %p\n", i, *pte, PTE2PA(*pte));
    rPrintf((pde_t*)PTE2PA(*pte), depth + 1);
  }
}

void vmprint(pagetable_t pt)
{
  printf("page table %p\n", pt);
  rPrintf(pt, 0);
}

int exec(char* path, char** argv){
  ...
  vmprint(p->pagetable);
  return argc;
  ...
}

A kernel page table per process (hard)

这个task的目的是为每一个进程创建一个内核页表,与全局的内核页表相比除了只有自己的内核栈之外没什么不同;这样当进程通过trap进入内核态时,我们可以直接使用进程的私有内核页表。至于为什么要这么做,做第三个task的时候就知道了。

为proc结构体增加两个字段kpagetablekstackpakpagetable是必须的而kstackpa是可选的是为了记录内核栈的物理地址,简化向内核页表添加映射以及释放内核栈物理空间;

// proc.h
struct proc {
	...
	pagetable_t kernelpgtbl;       // kernel page table
  char* kstackpa;               // physical address of kstack
	...
}

我们也不能省略全局内核页表,因为在系统初始化阶段即还没有用户进程阶段,内核仍然需要全局内核页表;我们需要扩展部分内核页表管理的接口,具体的为其添加一个pagetable_t的参数即可;然后就是在创建进程时为其创建私有的内核页表,并分配内核栈;在销毁进程时释放对应的内核页表,同时释放内核栈。

初始化内核页表

首先要扩展内核页表的初始化函数,使其为每个进程创建并初始化内核页表:

// kernel/vm.c

// create and init a kernel_pagetable
// 添加到def.h
pagetable_t kvminit2() {
  pagetable_t pgtbl = (pagetable_t)kalloc();
  memset(pgtbl, 0, PGSIZE);
  // uart registers
  kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // CLINT
  // kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);	// 普通进程的内核页表用不到这块虚拟地址空间,但是全局内核页表是需要的;

  // PLIC
  kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // map kernel text executable and read-only.
  kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);

  // map kernel data and the physical RAM we'll make use of.
  kvmmap(pgtbl, (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(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return pgtbl;
}

// create a direct-map page table for the kernel.
void
kvminit()
{
  kernel_pagetable = kvminit2();
  kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
}

// add a mapping to a kernel page table.
void
kvmmap(pagetable_t pgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if (mappages(pgtbl, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

通过扩展kvminit2()kvmmap()能够为任意进程创建并初始化其内核页表,此时每个内核页表都有相同的映射;kvminit()仍然需要在main()中初始化全局页表;

allocproc

allocproc()中为其创建私有内核页表,并且为其分配内核栈(因此不用在initproc()中将内核栈映射到全局页表了):

// kernel/proc.c 


static struct proc*
allocproc(void){
	...
	
found:
  p->pid = allocpid();
	p->kernelpgtbl = kvminit2();
  if (p->kernelpgtbl == 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Allocate a page for the process's kernel stack.
    // Map it high in memory, followed by an invalid
    // guard page.
    char* pa = kalloc();
    if (pa == 0)
      panic("kalloc");
    uint64 va = KSTACK((int)(0));
    p->kstack = va;
    p->kstackpa = pa;

  if (mappages(p->kernelpgtbl, (uint64)p->kstack, PGSIZE,
    (uint64)p->kstackpa, PTE_R | PTE_W) != 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  // ...
}

scheduler

这样在进程切换前,我们可以直接加载进程的私有内核页表而不用全局内核页表(修改scheduler()):

// kernel/proc.c
void
scheduler(void)
{
				...
				// 切换到进程独立的内核页表
        w_satp(MAKE_SATP(p->kernel_pagetable));
        sfence_vma(); // 清除快表缓存

        // 调度,执行进程
        swtch(&c->context, &p->context);
        // 切换回全局内核页表
        kvminithart();
        ...
}

在进程退出时我们需要及时切换回全局内核页表,这是因为在下一个task中,进程的内核页表中没有CLINT的映射,如果不切换回全局内核页表,会影响操作系统的资源管理能力。

freeproc

最后还需要在销毁进程时同时销毁其内核页表和释放内核栈:

// kernel/proc.c

static void
freeproc(struct proc* p)
{
  if (p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if (p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if (p->kernelpgtbl)
      proc_kfreepagetable(p->kernelpgtbl);
  if (p->kstack)
      kfree((void *)p->kstackpa);
  p->kstackpa = 0;
  p->pagetable = 0;
  p->kernelpgtbl = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

// 只销毁页表,不会释放内存空间
void
proc_kfreepagetable(pagetable_t pagetable)
{
  for(int i = 0; i < 512; i++){
        pte_t pte = pagetable[i];
        if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
            uint64 child = PTE2PA(pte);
            proc_kfreepagetable((pagetable_t)child);
            pagetable[i] = 0;
        }
    }
    kfree((void*)pagetable);
}

freewalk2()会在遍历页表时销毁各个目录所在的页面,但是不会销毁虚拟页面映射到的物理页面;

最终还有一个要修改:

// kernel/virtio_disk.c

void
virtio_disk_rw(struct buf *b, int write)
{
	...
	// buf0 is on a kernel stack, which is not direct mapped,
  // thus the call to kvmpa().
  disk.desc[idx[0]].addr = (uint64) kvmpa((uint64) &buf0);
  ...
}

观看注释可知,buf0位于内核栈中,因此需要修改kvmpa()不能让其直接引用全局内核页表,而是进程的私有内核页表:

#include "spinlock.h"#include "proc.h" // 为了调用myproc(),需要导入proc.h、spinlock.h// kernel/proc.cuint64kvmpa(uint64 va){  uint64 off = va % PGSIZE;  pte_t* pte;  uint64 pa;  pte = walk(myproc()->kernel_pagetable, va, 0);  // 需要解析内核栈的物理地址,因此必须使用进程的内核页表  if (pte == 0)    panic("kvmpa");  if ((*pte & PTE_V) == 0)    panic("kvmpa");  pa = PTE2PA(*pte);  return pa + off;}

Simplify copyin/copyinstr (hard)

这个task的目的是让我们将进程的地址空间映射同步到内核页表中,这样使得内核态可以直接对对用户态传进来的指针(逻辑地址)进行解引用(硬件实现的),避免了os查询进程页表的过程(只是软件实现的)。

要实现这样的效果,我们需要在每一处内核对用户页表进行修改的时候,将同样的修改也同步应用在进程的内核页表上,使得两个页表的程序段(0 到 PLIC 段)地址空间的映射同步。如下图所示,假设左图表示进程的内核页表(借用一下书中的图,事实上目前一个进程的内核页表只有当前进程的内核栈)我们要做的是利用进程内核页表[0…PLIC]这段虚拟地址空间映射进程的物理地址,使得内核可以直接访问进程的物理地址空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FuOFcynR-1638793994245)(/Users/liuzhilei/workspace-git/study-repo/6.S081/lab/pgtbl.assets/image-20211206183840124.png)]

实验要求中说明了,进程的虚拟地址空间不能超过PLIC;

因此需要在向内核页表同步映射时中加入检查,防止程序虚拟地址空间超过 PLIC,详见kvmcopymappings()


copyin、copyist

首先根据hints修改copyincopyist

// 声明新函数原型
int copyin_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len);
int copyinstr_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max);

int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable, dst, srcva, len);
}

int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  return copyinstr_new(pagetable, dst, srcva, max);
}

目前内核不需要再查询p->pagetable获取到用户地址空间后才能拷贝数据了;相应的我们也需要将进程虚拟地址空间映射同步到内核页表中。

kvmcopymappings、kvmdealloc

首先实现一些辅助方法,将进程页表的映射添加到内核页表:

// kernel/vm.c
// 注:需要在 defs.h 中添加相应的函数声明
// 将 src 页表的一部分页映射关系拷贝到 dst 页表中。
// 只拷贝页表项,不拷贝实际的物理页内存。
// 成功返回0,失败返回 -1
int
kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;

  // PGROUNDUP: 对齐页边界,防止 remap
  for(i = PGROUNDUP(start); i < start + sz; i += PGSIZE){
    if((pte = walk(src, i, 0)) == 0)
      panic("kvmcopymappings: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("kvmcopymappings: page not present");
    pa = PTE2PA(*pte);
    // `& ~PTE_U` 表示将该页的权限设置为非用户页
    // 必须设置该权限,RISC-V 中内核是无法直接访问用户页的。
    flags = PTE_FLAGS(*pte) & ~PTE_U;
    if(mappages(dst, i, PGSIZE, pa, flags) != 0){
      goto err;
    }
  }

  return 0;

 err:
  uvmunmap(dst, 0, i / PGSIZE, 0);
  return -1;
}

// 与 uvmdealloc 功能类似,将程序内存从 oldsz 缩减到 newsz。但区别在于不释放实际内存
// 用于内核页表内程序内存映射与用户页表程序内存映射之间的同步
uint64
kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
  if(newsz >= oldsz)
    return oldsz;

  if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
    int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
    uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
  }

  return newsz;
}

接下来就是在fork()、exec()、growproc()、userinit()中修改进程页表的地方,同步更新内核页表:

fork

// kernel/proc.c
int
fork(void)
{
  // ......

  // Copy user memory from parent to child. (调用 kvmcopymappings,将**新进程**用户页表映射拷贝一份到新进程内核页表中)
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0 ||
     kvmcopymappings(np->pagetable, np->kernelpgtbl, 0, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;

  // ......
}

exec

// kernel/exec.c
int
exec(char *path, char **argv)
{
  // ......

  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(p->name, last, sizeof(p->name));

  // 清除内核页表中对程序内存的旧映射,然后重新建立映射。
  uvmunmap(p->kernelpgtbl, 0, PGROUNDUP(oldsz)/PGSIZE, 0);
  kvmcopymappings(pagetable, p->kernelpgtbl, 0, sz);
  
  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);
  // ......
}

growproc

// kernel/proc.c
int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    uint64 newsz;
    if((newsz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // 内核页表中的映射同步扩大
    if(kvmcopymappings(p->pagetable, p->kernelpgtbl, sz, n) != 0) {
      uvmdealloc(p->pagetable, newsz, sz);
      return -1;
    }
    sz = newsz;
  } else if(n < 0){
    uvmdealloc(p->pagetable, sz, sz + n);
    // 内核页表中的映射同步缩小
    sz = kvmdealloc(p->kernelpgtbl, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

userinit

对于 init 进程,由于不像其他进程,init 不是 fork 得来的,所以需要在 userinit 中也添加同步映射的代码。

// kernel/proc.c
void
userinit(void)
{
  // ......

  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;
  kvmcopymappings(p->pagetable, p->kernelpgtbl, 0, p->sz); // 同步程序内存映射到进程内核页表中

  // ......
}

到这里,两个页表的同步操作就都完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值