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
的页,然后这个页的开头保存一个usyscal
l结构体,结构体中存放这个进程的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 的起始地址
至此,我们的实验三就结束了。