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
执行syscall
,syscall
根据调用号执行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
中。
kernel/proc.h
中的struct proc
添加一个字段,保存USYSCALL
的物理地址。kernel/proc.c
中的struct proc* allocproc
中申请USYSCALL
的物理地址,并向USYSCALL
中写入此进程的pid
,此处属于内核,物理地址与虚拟地址相同。kernel/proc.c
中的pagetable_t proc_pagetable
建立USYSCALL
在进程虚拟地址空间的映射,在void proc_freepagetable
完成虚拟地址空间的释放,void freeproc
完成物理地址空间的释放。- 需要注意共享内存页表的标志位应设置为
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,因此不会递归进进程的页,我们编写的函数,递归时进程的页都还在使用状态所以需要多加一个判断。
- 在
kernel/vm.c
中编写vmprint()
- 在
kernel/defs.h
声明原型 - 在
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 * PGSIZE
的buf
,此时这32个页应为未使用状态,紧接着向PGSIZE * 1
、PGSIZE * 2
、PGSIZE * 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;
}
结果
最后可以顺利的完成所以测试,有一个测试的时间比较长,差点以为是卡住了。