【mit 6.S081】学习笔记 Lab3 Page tables

Lab3 Page tables

页表的使用,了解虚拟内存。

虚拟内存页:page,每页有4KB大小,相应的物理内存页存放了相关数据。

虚拟内存页表:page_table存放了虚拟内存页到物理内存页的映射。

VA(virtual addresses):虚拟内存地址,页表里的"key"。

PTE(page table entries ):页表里的"value",根据PTE可以找到相关的物理内存页(的起始地址)。

PA(physical addresses):物理内存地址,由PTE和offset计算得出 。

PTE的flag:PTE_R、PTE_W、PTE_X、PTE_U。

Speed up system calls (easy)

xv6中,如果用户态调用系统调用,就会切换到内核态,这中间一定是有开销的,至少CPU要保存用户态进程的上下文,然后CPU被内核占有,系统调用完成后再切换回来。

这个实验就是要加速getpid(),提供的思路为为每一个进程多分配一个虚拟地址位于USYSCALL的页,然后这个页的开头保存一个usyscall结构体,结构体中存放这个进程的pid,具体可见memlayout.c中:

#define LAB_PGTBL 

#ifdef LAB_PGTBL
#define USYSCALL (TRAPFRAME - PGSIZE) // 很显然这是个起始地址

struct usyscall {
  int pid;  // Process ID
};
#endif

proc.h中修该结构体proc,向其中加入新页,这个页一定独立于进程页表。这样内核往这个页里写入数据的时候,用户程序就可以不经复杂的系统调用直接读取它了。

// Per-process state
struct proc
{
  ......
      
  struct trapframe *trapframe; // data page for trampoline.S
  struct usyscall *usyscall; // 我在这儿 !!!
  struct context context;     // swtch() here to run process
  ......
};

之后在proc.c中对这个页表进行初始化,即allocproc()中会分配出一些页,在这个函数中分配出所需要的usyscall

static struct proc *
allocproc(void)
{
  ......

  // Allocate a trapframe page.
  if ((p->trapframe = (struct trapframe *)kalloc()) == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  // 我在这儿 !!!
  if ((p->usyscall = (struct usyscall *)kalloc()) == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  ......
  }

然后因为当处于用户态寻址的时候都要经过页表硬件的翻译,所以usyscall也要映射在进程的pagetable上,即在proc_pagetables()中加入映射逻辑。

pagetable_t
proc_pagetable(struct proc *p)
{
  ......

  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  if (mappages(pagetable, TRAPFRAME, PGSIZE,
               (uint64)(p->trapframe), PTE_R | PTE_W) < 0)
  {
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
  // 我在这儿 !!
  // 记得指定权限位哦 PTE_R-只读 PTE_U-用户模式
  if (mappages(pagetable, USYSCALL, PGSIZE,
               (uint64)(p->usyscall), PTE_R | PTE_U) < 0)
  {
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  return pagetable;
}

接着在完成映射后,接着对其进行初始化,即在allocproc()中,为usyscall内容进行初始化。

static struct proc *
allocproc(void)
{
  ......

  // Allocate a trapframe page.
  if ((p->trapframe = (struct trapframe *)kalloc()) == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  // 这是初始化页表
  if ((p->usyscall = (struct usyscall *)kalloc()) == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall->pid = p->pid; // 我在这儿 !!!
  ......
  }

最后记得在进程回收时,释放这个页表,在freeproc()中加入即可。

static void
freeproc(struct proc *p)
{
  // 我在这儿 !!!
  if (p->usyscall)
    kfree((void *)p->usyscall);
  p->usyscall = 0;
  ......
}

至此我们这个实验的整个过程就完成了。

Print a page table (easy)

RISC-V 的逻辑地址寻址是采用三级页表的形式,9 bit 一级索引找到二级页表,9 bit 二级索引找到三级页表,9 bit 三级索引找到内存页,最低 12 bit 为页内偏移(即一个页 4096 bytes)。本实验就是模拟CPU查询页表的过程,对三级页表进行遍历,然后按一定格式输出。

defs.h中声明vmprintf()

// vm.c
......
void vmprint(pagetable_t pagetable, uint64 deepth); // 我在这儿 !!!

因为是递归打印页表,而在xv6有一个递归释放页表的函数freewalk(),所以只需要将释放部分的代码改为打印就完成了。

static char *deepth_set[] = {
    [0] = "..",
    [1] = ".. ..",
    [2] = ".. .. .."};

void vmprint(pagetable_t pagetable, uint64 deepth) // deepth是指递归深度
{
  if (deepth > 2)
  {
    return;
  }
  if (deepth == 0)
  {
    printf("page table %p\n", pagetable);
  }

  char *deepth_print = deepth_set[deepth];

  for (int i = 0; i < 512; i++)
  {
    pte_t pte = pagetable[i];
    if (pte & PTE_V) // 页表项有效
    {
      printf("%s%d: pte %p pa %p\n", deepth_print, i, pte, PTE2PA(pte));
      uint64 child = PTE2PA(pte); // 递归打印子节点
      vmprint((pagetable_t)child, deepth + 1);
    }
  }
}

最后在exec.c中在exec()就返回前打印页表。

int exec(char *path, char **argv)
{
  ......
      
  if (p->pid == 1)
  {
    vmprint(p->pagetable,0);
  } // 我在这儿 !!!

  return argc; // this ends up in a0, the first argument to main(argc, argv)

  ......
}
Detecting which pages have been accessed (hard)

实现一个系统调用pgacccess,其作用是从一个用户页表地址开始,搜索所有被访问过的页并返回一个bitmap来显示这些页是否被访问过。比如说,如果第二个页被访问过了,bitmap里从右往左数第二个bit就是1。添加一个系统调用的过程,之前已经详细说明了,这里不做过多赘述。

void
pgaccess_test() // 这玩意是测试代码,我们可以通过这个代码来明确我们要做什么
{
  char *buf; // 这玩意是传给sys_pgaccess()的第一个参数,是一个用户指针
  unsigned int abits;
  printf("pgaccess_test starting\n");
  testname = "pgaccess_test";
  buf = malloc(32 * PGSIZE); // 长度限制在 0-32
  if (pgaccess(buf, 32, &abits) < 0)
    err("pgaccess failed");
  buf[PGSIZE * 1] += 1;
  buf[PGSIZE * 2] += 1;
  buf[PGSIZE * 30] += 1;
  if (pgaccess(buf, 32, &abits) < 0)
    err("pgaccess failed");
  if (abits != ((1 << 1) | (1 << 2) | (1 << 30)))
    err("incorrect access bits set");
  free(buf);
  printf("pgaccess_test: OK\n");
}

首先,在riscv.h中,添加对PTE_A的宏定义。

#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_A (1L << 6) // 我在这儿 !!! 至于为啥是6嘞,翻书去吧你!

其次,在sysproc.c中补全sys_pgaccess()

#ifdef LAB_PGTBL
int sys_pgaccess(void)
{
  uint64 addr;
  int length; 
  int bitmask;
  if (argaddr(0, &addr) < 0) // 页表第一页的起始地址
  {
    return -1;
  }
  if (argint(1,&length) < 0) // 要考察的页表长度
  {
    return -1;
  }
  if (argint(2, &bitmask)) // 位掩码 第一页对应最低有效位
  {
    return -1;
  } // 接受相关参数
  if (length > 32 || length < 0)
  {
    return -1;
  } // 判断长度限制范围
  int ret = 0; // 中间变量
  struct proc *p = myproc(); // 获取当前进程
  for (int i = 0; i < length; i++)
  {
    int va = addr + i * PGSIZE;
    int bitmask = vmpgaccess(p->pagetable, va); 
    ret = ret | bitmask << i;
  } // 主体代码
  if (copyout(p->pagetable, bitmask, (char *)&ret, sizeof(ret)) < 0)
  {
    return -1;
  }
  return 0;
}
#endif

接着,在defs.h中声明vmpgaccess()

// defs.h
......
void vmprint(pagetable_t pagetable, uint64 deepth); // 这是lab2的哦 !!!
int vmpgaccess(pagetable_t pagetable, uint64 va); // 我在这儿 !!!

最后,在vm.c中实现vmpgaccess()

// vm.c
int vmpgaccess(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  if (va >= MAXVA)
  {
    return 0;
  }
  pte = walk(pagetable, va, 0);
  if (pte == 0)
  {
    return 0;
  }
  if ((*pte & PTE_A) != 0)
  {
    *pte = *pte & (~PTE_A); // 清空 PTE_A 标志位 防止数据干扰
    return 1;
  }
  return 0;
}
// 这个代码完全可以参考 walkfree()
// 提示中提到的 walk() 其实就是实现三级页表的机制

解释一下walk()吧。

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); // 检测 PTE_V 即页表的合法性
    }
    else
    {
      if (!alloc || (pagetable = (pde_t *)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE); 
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}
// 参数: 页表 虚拟地址 
// 返回值:   获取 PTE 的起始地址

至此,我们的实验三就结束了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值