zl程序教程

您现在的位置是:首页 >  其它

当前栏目

MIT_6.s081_Lab3:Xv6 and PageTable

and MIT S081 xv6
2023-06-13 09:15:43 时间

MIT_6.s081_Lab3:Xv6 and PageTable

于2022年3月5日2022年3月5日由Sukuna发布

Lab3_1 Speed Up the system calls

一些操作系统(例如 Linux)通过在用户空间和内核之间共享只读区域中的数据来加速某些系统调用。 这消除了在执行这些系统调用时对内核交叉的需要。

创建每个进程时,在 USYSCALL(memlayout.h 中定义的 VA)映射一个只读页面。 在这个页面的开始,存储一个struct ussyscall(也在memlayout.h中定义),并初始化它来存储当前进程的PID。

  • 可以在 kernel/proc.c 中的 proc_pagetable() 中执行映射。
  • 只读的权限位要确保正确
  • mappages() 是一个有用的实用程序。
  • 不要忘记在 allocproc() 中分配和初始化页面。
  • 确保在 freeproc() 中释放页面。
1) 确认usyscall的结构体是什么,其实存储的就是pid.
struct usyscall {
  int pid;  // Process ID
};
2) 为每个进程结构添加usyscall的元素,这个结构存储在一个新的页里面.
3) 打开proc.c,依葫芦画瓢给usyscall结构体申请一个页面.
// alloc a page that store usyscall proc
  if((p->usyss = (struct usyscall *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
4) 依据提示,我们在proc_pagetable中依葫芦画瓢来进行地址的映射,注意,如果映射失败是要把之前俩都给取消掉
    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;
    }

其中mappages(表,虚拟地址,页大小,物理地址,权限)

#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

权限的编码有几种方式,分别用五位0-1表示,从左到右都是可运行,可读,可写,只可以内核用,用户可以用.

5) 在进程初始化的时候添加初始化的代码.初始化usyscall的结构.
p->usyscall->pid = p->pid;
6) 进程结束的时候,free掉进程的时候把这一页也free掉
if (p->usyss) kfree((void *)p->usyss);
p->usyss = 0;
7) 进程结束的时候,依葫芦画瓢把映射关系给取消掉.
uvmunmap(pagetable, USYSCALL, 1, 0);

Lab3_2 Print a page table

在这部分实验中,您将向 xv6 添加一个新功能,该功能通过检查 RISC-V 页表中的访问位来检测并向用户空间报告此信息。定义一个名为 vmprint() 的函数。 它应该接受一个 pagetable_t 参数,并以下面描述的格式打印该页表。

page table 0x0000000087f6e000//二级页表
 ..0: pte 0x0000000021fda801 pa 0x0000000087f6a000//二级页表的表项,进入一级页表
 .. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000//一级页表的表项,进入页表
 .. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
 .. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
 .. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
 ..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
 .. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
 .. .. ..509: pte 0x0000000021fdd813 pa 0x0000000087f76000
 .. .. ..510: pte 0x0000000021fddc07 pa 0x0000000087f77000
 .. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

在exec.c中的return argc;之前插入if(p->pid==1) vmprint(p->pagetable) 语句来输出第一个进程的页表.

首先要确定的第一点就是,这个页表其实实际上是一种多级页表的结构,就是二级页表存储的表项(PPN)其实是一级页表的第一个PTE的地址,一级页表的表项才是pa.具体的多级页表的解释可以参考我其他的博客或者翻阅操作系统或者组员书.

首先我们知道,PTE是一个长度为64的整数而已.其中存储了各种各样的信息,这些信息可以通过调用宏来实现:

#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10
#define PTE2PA(pte) (((pte) >> 10) << 12)
#define PTE_FLAGS(pte) ((pte) & 0x3FF)

其实本质上来说,就是xv6系统的PTE除去后10位和前12位就是pa.

所以说这个东西就可以转化为基本的递归.对于二级页表,对于每一个表项就进入到一级页表再遍历.所以说本质上这就是一个树,二级页表作为根,有若干个儿子,也就是一级页表,一级页表也有若干个儿子…那遍历树怎么遍历,一般来说就是用递归的手段

● 可以将vmprint( )放在kernel/vm.c中。 ● 使用kernel/riscv.h文件末尾的宏。 ● 函数freewalk可能是鼓舞人心的(可以仿照该函数来写vmprint)。 ● 在kernel/defs.h中定义vmprint的原型,以便可以从exec.c调用它。 ● 在printf调用中使用%p输出完整的64位十六进制PTE和地址,如示例所示。

1) 根据提示,在exec的return argc之前添加一段:
if(p-pid==1){
	vmprint(p->pagetable);
}
2) 在def.h中添加对于vmprint的定义
3) 修改vm.c,仿照freewalk改造函数.
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 ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
                vmprintlevel((pagetable_t)child, level - 1);
            }
        }
    }
}

void vmprint(pagetable_t pt) {
    printf("page table %p\n", pt);
    vmprintlevel(pt, 2);
}

思想就是递归,递归获得下一级的地址,本质上是一个DFS.有下一级的页表就进入下一级进行遍历.

Lab3_3 Detecting which pages have been accessed

在本部分的实验中,你将向xv6添加一个新特性,通过检查RISC-V页表中的访问位来获取信息并向用户空间报告这些信息。

实现pgaccess()函数,它是一个系统调用,会返回哪些页面已经被访问.其中第一个参数就是从哪个虚拟地址开始检查,第二个参数就是检查几个页面,结果传递给第三个参数,第三个参数是位掩码,其中第几位为1表示第几个页面已经被访问了.

● 首先在kernel/sysproc.c中实现sys_pgaccess( )。 ● 你需要使用argaddr()和argint()解析参数。 ● 对于输出位掩码,在内核中存储一个临时缓冲区并在填充正确的位后将其复制给用户(通过copyout())更容易。 ● 可以设置可扫描页数的上限。 ● kernel/vm.c中的walk()对于查找正确的PTE非常有用。 ● 你需要在kernel/riscv.h中定义PTE_A,即访问位。请参阅RISC-V手册以确定其值。 ● 如果PTE_A已设置,在检查后务必清除它。否则,将无法确定自上次调用pgaccess()以来是否访问了页面(即,该位将被永久设置)。 ● 利用vmprint()可方便地调试页表。

1) 参阅RISC-V手册,确定PTE_A是什么.
#define PTE_A (1L << 6)
2) 完成sys_pgaccess,在这里主要是处理参数.利用argaddr和argint处理参数.
uint64 sys_pgaccess(void) {
    // lab pgtbl: your code here.
    // get argument
    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);
}
3) 读取当前虚拟地址,找到对应的页表项目的函数找到:walk

使用方法:te = (pte_t*)walk(pagetable, ((uint64)pg) + (uint64)PGSIZE * i, 0);

4) 读取接着根据pte表项,找到PTE_A位.
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 = (pte_t*)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));
}