Mit6.s081 lab3

部分前置知识:

首先是物理地址布局理解

物理地址图
──────────────────────────────────────────────────────────────
地址范围           | 描述
──────────────────────────────────────────────────────────────
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的调用。

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值