MIT 6.1810 Lab: page tables

lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/pgtbl.html

xv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf

Book learning

Xv6 使用 Sv39 RISC-V 标准, 即使用39位的虚拟地址。前27位作为页表项索引,后12位作为页内偏移。页表项记录一个44位的实页号,因此物理地址共使用56(44+12)位。

Xv6 使用三级页表和一些标志位,V表示是否存在;U表示是否属于用户空间;G表示全局即该页面是否在不同进程地址空间共享;A、D为使用和脏位,用于页面置换。每张页表共有512个页表项,页表与页同大小都为4KB。

Xv6 使用 satp 寄存器记录页目录基址,页表存放在内核区,内核区域的虚拟地址和物理地址是相同的,因此内核很容易修改存放在物理内存中的页表或者进行内核的其他基本活动。每个进程有自己的内核栈,Guard Page不可写用于防止内核栈溢出。

mappages 是页表创建的核心函数,它接收页表指针,虚拟地址基址,物理地址基址,映射大小,标志位。根据映射大小建立足够页表项,从接收物理地址基址开始,创造页表项,填入页表。walk函数用于完成每个页表项的建立。

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned. Returns 0 on success, -1 if walk() couldn't
// allocate a needed page-table page.
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
    uint64 a, last;
    pte_t *pte;

    if(size == 0)
        panic("mappages: size");

    a = PGROUNDDOWN(va);
    last = PGROUNDDOWN(va + size - 1);
    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;
}

kvmmake 是创建内核页表的核心函数,它申请一块物理内存,作为内核页表,然后调用kvmmap创建页表项建立各个模块从虚拟地址到物理地址的映射。pagetable_t是页目录指针,指向三级页表的顶级页表基址,它间接容纳整个进程的地址空间,因此可以在其中建立任意合规的映射关系。

// Make a direct-map page table for the kernel.
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, 0x400000, 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;
}

walk是页表项创建的核心函数,描述每个页页表的创建过程。pte_t *pte = &pagetable[PX(level, va)]获取页表项地址,如果获取的页表项是有效的(即已经存在),则获取页表项指向的页表(处理多级页表),否在申请一块内存,并将申请得到的内存地址写入页表项。

// Return the address of the PTE in page table pagetable
// that corresponds to virtual address va.  If alloc!=0,
// create any required page-table pages.
//
// The risc-v Sv39 scheme has three levels of page-table
// pages. A page-table page contains 512 64-bit PTEs.
// A 64-bit virtual address is split into five fields:
//   39..63 -- must be zero.
//   30..38 -- 9 bits of level-2 index.
//   21..29 -- 9 bits of level-1 index.
//   12..20 -- 9 bits of level-0 index.
//    0..11 -- 12 bits of byte offset within the page.
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)];
}

kalloc用于分配物理地址,本人的理解它并不是真正的分配,而是表示这片内存可以使用,是对整个物理内存使用情况的一种标记。要想使用物理内存,页表的有效位必须为1(即使内核区域使用直接映射,cpu访问内存时也需要页表间接访问)。kalloc 分配的起始位置为内核程序的终点,freerange(end, (void*)PHYSTOP);end变量由链接器根据连接脚本设置,PROVIDE(end = .);,这里的.代表程序终点。

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
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); // fill with junk
  return (void*)r;
}

Speed up system calls

本部分希望我们使用共享内存,加速一些系统调用的执行。我们不妨先看看传统的getpid系统调用的执行流程。

getpid作为系统调用,声明在user.h中,定义在usys.S中,需要通过陷入的方式进入内核,由内核代为执行。

000000000000037c <getpid>:
. getpid
getpid:
 li a7, SYS_getpid
 37c:	48ad                	li	a7,11
 ecall
 37e:	00000073          	ecall
 ret
 382:	8082                	ret

中断程序usertrap执行syscallsyscall根据调用号执行sys_getpid,返回值填入p->trapframe->a0

void
usertrap(void)
{
    ......
    syscall();
	......
}

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

uint64
sys_getpid(void)
{
  return myproc()->pid;
}

为了加速这一过程,可以在每个进程创建时,在其虚拟地址空间中划定一部分只读的页面与内核共享,即USYSCALL。如下图所示,其中TRAMPOLINE为陷阱代码完成用户态到内核态的跳转,TRAPFRAME为陷阱帧完成跳转过程中的现场保护。内核在创建进程时向TRAMPOLINE写入数据,进程在执行系统调用时,直接读取TRAMPOLINE中的数据,而不陷入内核。

USYSCALL的地址定义在memlayout.h,其映射的物理地址为进程在创建过程中使用kalloc动态获取的地址,因此每个用户进程USYSCALL的虚拟地址是固定的,物理地址是动态的,物理地址记录在struct proc中。

  1. kernel/proc.h中的struct proc添加一个字段,保存USYSCALL的物理地址。
  2. kernel/proc.c中的struct proc* allocproc中申请USYSCALL的物理地址,并向USYSCALL中写入此进程的pid,此处属于内核,物理地址与虚拟地址相同。
  3. kernel/proc.c中的pagetable_t proc_pagetable建立USYSCALL在进程虚拟地址空间的映射,在void proc_freepagetable完成虚拟地址空间的释放,void freeproc完成物理地址空间的释放。
  4. 需要注意共享内存页表的标志位应设置为PTE_R | PTE_U,即只读和用户可访问。
#kernel/proc.h

+++ typedef uint64 usyscall_t;

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)

+++  usyscall_t usyscall;            //Physical Address of usyscall
};
#kernel/proc.c

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
+++	if(p->usyscall)
+++		kfree((void*)p->usyscall);
+++	p->usyscall = 0;
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 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;
}

// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
+++  uvmunmap(pagetable, USYSCALL, 1, 0);
  uvmfree(pagetable, sz);
}
#kernel/proc.c

// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  pagetable = uvmcreate();
  if(pagetable == 0)
    return 0;

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }

  // map the trapframe page just below the trampoline page, 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;
  }

+++	// map the usyscall page just below the trapframe page
+++	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;
}

Print a page table

这部分需要我们编写一个函数vmprint(),可以打印进程的页表。编写方法模仿freewalk,在freewalk最后一级页表的页表项的有效位正常情况为0,因此不会递归进进程的页,我们编写的函数,递归时进程的页都还在使用状态所以需要多加一个判断。

  1. kernel/vm.c中编写vmprint()
  2. kernel/defs.h声明原型
  3. exec.c中添加if(p->pid==1) vmprint(p->pagetable)
// Recursively print page-table pages.
void _vmprint(pagetable_t pagetable, int layer){
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    if(pte & PTE_V){
      switch (layer)
      {
      case 1:
        printf("..");
        break;
      
      case 2:
        printf(".. ..");
        break;
      
      case 3:
        printf(".. .. ..");
        break;

      default:
        return;
        break;
      }
      uint64 child = PTE2PA(pte);
      printf("%d: pte %p pa %p\n",i,(void *)pte,(void *)child);
      // this PTE points to a lower-level page table.
      _vmprint((pagetable_t)child,layer+1);
    }
  }
}

// print page-table pages.
void vmprint(pagetable_t pagetable){
  printf("page table %p\n",(void*)pagetable);
  _vmprint(pagetable,1);
}

Detect which pages have been accessed

首先观察pgaccess_test中是如何使用pgaccess的。在pgaccess_test申请了一个大小为32 * PGSIZEbuf,此时这32个页应为未使用状态,紧接着向PGSIZE * 1PGSIZE * 2PGSIZE * 30中进行读写,此时pgaccess得到的abits应有三个位为1。

void
pgaccess_test()
{
  char *buf;
  unsigned int abits;
  printf("pgaccess_test starting\n");
  testname = "pgaccess_test";
  buf = malloc(32 * PGSIZE);
  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");
}

检查一个页面是否被访问可以通过检测标志位PTE_A完成,PTE_A标志位由硬件自动写入,我们只需要检测哪些页表项的PTE_A存在,并在统计之后清零。walk函数返回对应虚拟地址的PTE,对标志位进行检查和修改即可完成。本题的难点主要在调试。

int
sys_pgaccess(void)
{
  uint64 buf;
  int num;
  uint64 abits;
  uint32 bits = 0;

  argaddr(0,&buf);
  argint(1,&num);
  argaddr(2,&abits);

  struct proc * p = myproc();
  pagetable_t pgtbl = p->pagetable;

  pte_t* pte;
  for(int i = 0;i < num && i < 32 ;i++){
    pte = walk(pgtbl,buf+PGSIZE*i,0);
    if(*pte & PTE_A){
      bits |= (1UL << i);
      *pte &= (~PTE_A);
    }
  }
  if(copyout(pgtbl,abits,(char*)&bits,4)<0){
    printf("copyout fail");
  }

  return 0;
}

结果

最后可以顺利的完成所以测试,有一个测试的时间比较长,差点以为是卡住了。

posted @ 2024-02-01 20:28  benoqtr  阅读(67)  评论(0编辑  收藏  举报