2021-xv6实验记录(三)
本次实验,将进行三个子任务:
- 系统调用性能优化
- 打印进程 1 的页表
- 跟踪进程的内存访问位。
官方实验指南:在这里!!!
开始实验之前首先切换到官方的pgtbl分支
$ git fetch
$ git checkout pgtbl
$ make clean
实验【Speed up system calls】
一、实验前提示
- 在
kernel/proc.c
文件里的alloc_proc()
函数里,加入对USYSCALL
页面的创建过
程(可以仿照 TRAPFRAME 页面的构建方法来写相关的代码),并在之后对 usyscall
结构体的 pid 成员变量进行赋值 - 在
kernel/proc.c
文件里的proc_pagetable()
里,加入对USYSCALL
页面的映射过
程并赋予适当的权限(可以仿照 trapframe 和 TRAMPOLINE 的映射方法); - 映射的方法用到的核心函数为 mappages();
- 在进程退出或者分配内存失败的情况下,记得释放 USYSCALL 和相关的页面,防止
内存泄漏(例如在 freeproc()函数里添加相关的内存撤销/回收语句); - 根据上述描述,ugetpid()的核心逻辑其实就是在用户层对进程的 USYSCALL 页面进
行访问,可以查看 xv6 源码目录下的 user/pgtbltest.c 文件是如何对其进行
访问的
**简述题意:**给系统调用函数ugetpid()
提速,方法是给每个进程的单独内存空间里添加一个USYSCALL
页面,而里头存放一个系统函数会经常使用的数据,这里专指pid
,而我们把pid
存放在struct usyscall
中。
- 通过上述方法,
ugetpid()
需要用到pid
时,会在用户态使用USYSCALL
页面直接调用,而不用切换到内核态
二、实验过程
首先打开kernel/memlayout.h
,可以看到下述代码
/* kernel/memlayout.h */
#ifdef LAB_PGTBL
#define USYSCALL (TRAPFRAME - PGSIZE) // USYSCALl = 3FFFFFD000
struct usyscall {
int pid; // Process ID
};
#endif
这个USYSCALL页一定是独立于进程页表的一个页,把定义加到kernel/proc.h
中:
struct proc{
struct spinlock lock;
struct usyscall *usyscall; //新添加的代码
...
}
然后去kernel/proc.c
中初始化这个页面,allocproc()
中会分配一些页,在这个函数里面分配出需要的usyscall
static struct proc * allocproc(void){
...
//原有代码
...
// 新添加的代码!!
if ((p->usyscall = (struct usyscall *)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
// An empty user page table.
p->pagetable = proc_pagetable(p);
...
}
用户态寻址的时候都要经过页表硬件的翻译,所以usyscall
也要映射在进程的pagetable
上,刚刚加入的位置的下面就有proc_pagetable(p)
用于map page
,在kernel/proc.c
中加入映射逻辑
pagetable_t proc_pagetable(struct proc *p){
...
//新添加的代码
if (mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall),
PTE_R | PTE_U) < 0) {
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
映射完成了之后就要对其进行初始化,回到kernel/proc.c
,加入代码
static struct proc * allocproc(void){
...
//新添加的代码
p->usyscall->pid = p->pid;
return p;
}
根据提示:进程回收的时候,该页面也要释放,在freeproc()
中加入释放即可
static void freeproc(struct proc*p){
...
//新添加的代码
if (p->usyscall) kfree((void *)p->usyscall);
p->usyscall = 0;
}
并且在函数proc_freepagetable()
中解除映射,不然启动的时候会报freewalk panic
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
...
//新添加的代码
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}
三、实验结果的验证
打开终端,make qemu启动xv6系统,检查是否报错,如果没有遇见错误,在 xv6 的终端里敲:pgtbltest
或者./pgtbltest
,看看是否能够通过对 ugetpid()的调用测试
实验【Print a page table】
一、实验前提示
- 输出页表功能由
vmprint()
函数实现,它的参数只有一个,类型为
pagetable_t
,其定义在kernel/riscv.h
的末尾; - 在
kernel/exec.c
文件里,在return argc;
语句之前,也即是 exec()函数的
接近末尾处,加入if(p->pid==1) vmprint(p->pagetable);,以便只打印 pid 为 1 进程页表; - 在
kernel/vm.c
里,找个地方实现vmprint()
,它的逻辑就是输出当前进程的
页表; - 在文件
kernel/vm.c
里,有个现有的函数叫做freewalk()
——你的 vmprint()
代码可以参考和借鉴; - 在文件
kernel/riscv.h
的末尾,有几个宏定义,在解题的时候会用的上; - 输出内存地址的时候,可以在 printf 里用%p 来格式化指针(地址),它的好
看程度和图 3.2 的地址输出一样美丽;
二、实验过程
实验解决思路:递归遍历页表,碰到有效的就遍历进下一层页表
根据提示在kernel/vm.c
文件中添加如下代码
void vmprintlevel(pagetable_t pt, int level) {
char *delim = 0;
if (level == 2) delim = "..";
if (level == 1) delim = ".. ..";
if (level == 0) delim = ".. .. ..";
for (int i = 0; i < 512; i++) {
pte_t pte = pt[i];
if ((pte & PTE_V)) {
// this PTE points to a lower level page table.
printf("%s%d: pte %p pa %p\n", delim, i, pte, PTE2PA(pte));
uint64 child = PTE2PA(pte);
if (level != 0) {
vmprintlevel((pagetable_t)child, level - 1);
}
}
}
}
void vmprint(pagetable_t pt) {
printf("page table %p\n", pt);
vmprintlevel(pt, 2);
}
记得在kernel/defs.h
文件中添加函数的声明
...
void vmprintlevel(pagetable_t pt, int level); //新添加
void vmprint(pagetable_t pt); //新添加
写完了函数还要记得在make的时候启动输出因此找到kernel/exec.c 文件里,在
return argc;`语句之前,也即是 exec()函数的
接近末尾处,加入if(p->pid==1) vmprint(p->pagetable);
// 文件kernel/exec.c
int
exec(char *path, char **argv)
{
...
if(p->pid==1) { //新添加的代码
vmprint(p->pagetable);
}
return argc; // this ends up in a0, the first argument to main(argc, argv)
...
三、实验结果的验证
此时我们在终端使用make qemu启动后可以看到如下效果:
实验【Detecting which pages have been accessed】
一、实验前提示
- 在
kernel/sysproc.c
里实现pgaccess()
系统调用,名为sys_pgaccess()
如果你已经切换到了本 lab 的代码分支,那么该系统调用的声明已经完成,你只
需实现该系统调用本体即可; - 有两个内核函数可以用到,分别是:
argaddr()
和argint()
,它们的作用都是把用
户层的系统调用参数通过一个安全的方法导入至内核使用;这两个内核函数的定
义在kernel/syscall.c
- 因为这里用
bitmask
来保存指定页面范围内的访问情况,所以pgaccess()
系统调
用在内核层收集完信息后,我们需要把这个信息拷贝回用户层,这里用到的一个
函数叫做copyout()
,它的实现在kernel/vm.c
的末尾**(还记得 lab2 中的**
Sysinfo 吗?) - 可以设定
pgaccess()
系统调用所涉及的页面数量最大值,比如 32; kernel/vm.c
文件里的walk()
函数可以在本次任务中发挥较大的用处,它的执行
逻辑是:给定一个 VA(Virtual Address),返回其在指定页表内的 L0 级页目录的
PTE 地址;你可以用它来检测返回值的对应访问位是否为 1;- 需要在
kernel/riscv.h
里定义页表的访问位,也即PTE_A
,否则你的默认页表里
没有PTE_A
位可供系统调用查询 - 在 pgaccess()执行期间,确保 PTE_A 为 1 的页面在 check 之后置其为 0
二、实验过程
主要思路就是用户调用系统调用的时候,我们去查找页表,获得对应的PTE
,然后检查PTE_A
(需要自己定义),然后决定是否在答案设置对应有效位。
首先我们自己定义PTE_A
,打开kernel/riscv.h
文件
...
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_A (1L << 6) // 改行代码为新添加的内容
在kernel/sysproc.c
中完善sys_pgaccess()
#ifdef LAB_PGTBL
uint64 sys_pgaccess(void) { //函数体的内容都是新添加的
// lab pgtbl: your code here.
uint64 buf;
int number;
uint64 ans;
if (argaddr(0, &buf) < 0) return -1;
if (argint(1, &number) < 0) return -1;
if (argaddr(2, &ans) < 0) return -1;
return pgaccess((void*)buf, number, (void*)ans);
}
#endif
看到上一步使用了pgaccess函数因此在kernel/defs.h
中需要添加声明
uint64 pgaccess(void *pg, int number, void *store);
然后去kernel/proc.c
中加入pgaceess()
的定义
//新添加的函数
uint64 pgaccess(void *pg, int number, void *store) {
struct proc *p = myproc();
if (p == 0) {
return 1;
}
pagetable_t pagetable = p->pagetable;
int ans = 0;
for (int i = 0; i < number; i++) {
pte_t *pte;
pte = walk(pagetable, ((uint64)pg) + (uint64)PGSIZE * i, 0);
if (pte != 0 && ((*pte) & PTE_A)) {
ans |= 1 << i;
*pte ^= PTE_A; // clear PTE_A
}
}
// copyout
return copyout(pagetable, (uint64)store, (char *)&ans, sizeof(int));
}
因为上一步过程使用了一个walk函数因此我们需要在kernel/defs.h
中添加walk函数的声明
pte_t* walk(pagetable_t,uint64,int); //新添加的函数声明
三、实验结果的验证
使用命令make grade
检测前面三个实验是否完全通过
注意:需要创建一个文件
answers-pgtbl.txt
文件用于保存答案否则make grade的时候会在在test answer - pgtbl.txt报错,同时创建了文件之后在里面随便敲入多个字符保留答案输出空间即可:即:在xv6 - labs - 2021目录下
- touch answers-pgtbl.txt
- vim answers-pgtbl.txt
- 随便敲入一些内容
~~