实验:Lab: System calls

实验开始之前需要将git分支切换到syscall分支不然有些文件你是没有的

1
2
3
$ git fetch
$ git checkout pgtbl
$ make clean

Speed up system calls (easy)

实验要求

一些操作系统(例如Linux)通过在用户空间和内核之间共享只读区域中的数据来加快某些系统调用。这消除了执行这些系统调用时内核交叉的需要。为了帮助您了解如何将映射插入到页表中,第一个任务是为xv6中的getpid()系统调用实现此优化。

创建每个进程时,将一个只读页面映射到USYSCALL(在memlayout.h中定义的虚拟地址)。在该页的开始,存储一个usyscall结构(也在memlayout.h中定义),并初始化它以存储当前进程的PID。对于本实验,用户空间端提供了ugetpid(),它将自动使用usycall映射。如果ugetpid测试用例在运行pgtbltest时通过,您将获得这部分实验的全部学分。

HINS

  • 在kernel/proc.c中的proc_pagetable()中执行映射。

  • 选择允许用户空间只读取页面的权限位。

  • 您可能会发现mappages()是一个有用的实用程序。

  • 不要忘记在allocproc()中分配和初始化页。

  • 确保在freeproc()中释放页。(同时也需要在proc_freepagetable()中释放PTE)

实现

这个实验主要是在用户进程中提供一个共享页(page),加快访问的速度,主要思想是不直接系统调用的执行,而是通过USYSCALL这个固定的虚拟地址,通过虚拟地址翻译找到指定页。

如下图所示,不同进程的同一虚拟地址通过页表映射到了不同的物理页。

  1. Trampoline页:物理页中只有一页,保存的是进出内核的代码。内核与用户共享且虚拟地址与物理地址一对一映射
  2. Trapframe页:一个进程一页,保存寄存器数据。每一个进程一个,相同的虚拟地址映射不同的物理页。(USYSCALL页的实现方式也可以参照于该页的实现)

lab3.2.png

  1. 在proc.h中添加一个usyscall结构体指针,像trapframe一样
    1
    2
    3
    4
    5
    6
    7
    ---proc.h---
    // Per-process state
    struct proc {
    ...
    struct usyscall *sc;//usyscall page
    ...
    };
  2. 在allocproc()为进程分配一个物理页,然后将usyscall结构体放入刚创建的页(page)字节数组中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ---proc.c---
    static struct proc*
    allocproc(void)
    {
    ...
    if((p->sc = (struct usyscall *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
    }

    struct usyscall u;
    u.pid = p->pid;
    *p->sc = u;
    ...
    }
  3. 在proc_pagetable中,在进程页表中建立USYSCALL页的PTE,创建映射。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ---proc.c---
    pagetable_t
    proc_pagetable(struct proc *p)
    ...
    if(mappages(pagetable, USYSCALL, PGSIZE,
    (uint64)(p->sc), PTE_R | PTE_U ) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
    }
    ...
    }
  4. 分别在proc_freepagetable()、freeproc()中清除USYSCALL页的PTE、清除物理页。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---proc.c---
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
...
uvmunmap(pagetable, USYSCALL, 1, 0);
}

static void
freeproc(struct proc *p)
{
...
if(p->sc)
kfree((void*)p->sc);

p->sc = 0;
...
}

Print a page table (easy)

要求

为了帮助我们可视化RISC-V页表,也许还可以帮助将来的调试,第二个任务是编写一个打印页表内容的函数。

定义一个名为vmprint()的函数。它应该接受一个pagetable_t参数,并以下面描述的格式打印该pagetable。在execute .c中,在返回argc之前插入if(p->pid==1) vmprint(p->pagetable),打印第一个进程的页表。如果你通过了这部分实验的打印测试,你就可以得到全部的学分。

当我们启动xv6时,它应该像这样打印输出,描述第一个进程刚刚完成exec()ing init时的页表:

1
2
3
4
5
6
7
8
9
10
11
12
13
page table 0x0000000087f6b000
..0: pte 0x0000000021fd9c01 pa 0x0000000087f67000
.. ..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. .. ..0: pte 0x0000000021fda01b pa 0x0000000087f68000
.. .. ..1: pte 0x0000000021fd9417 pa 0x0000000087f65000
.. .. ..2: pte 0x0000000021fd9007 pa 0x0000000087f64000
.. .. ..3: pte 0x0000000021fd8c17 pa 0x0000000087f63000
..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..509: pte 0x0000000021fdcc13 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh

第一行显示vmprint的参数。在此之后,每个PTE都有一行,包括引用树中较深的页表页的PTE。每个PTE行由一些“..”缩进,表示其在树中的深度。每个PTE行显示其页表页中的PTE索引、PTE位以及从PTE中提取的物理地址。不要打印无效(PTE_A)的PTE。在上面的示例中,顶级页-表页具有条目0和255的映射。条目0的下一层仅映射了索引0,而该索引0的底层映射了条目0、1和2。

HINS

  • 可以将vmprint()放在kernel/vm.c中。

  • 使用kernel/riscv.h文件末尾的宏。

  • freewalk的是递归遍历页表的,因此vmprint可以按照其代码来实现。

  • 在kernel/def .h中定义vmprint的原型,以便可以从exec调用它。

  • 在printf调用中使用%p来打印完整的64位十六进制pte和地址,如示例所示。

实现

大致的过程就是通过,在vm.c 文件中实现一个函数名为vmprint,输入的参数为,一级(根)页表的物理地址。在vmprint函数中通过遍历一级页表,获得二级页表的PTE,判断PTE的PTE_A位然后输出进行个格式输出。同理再遍历二级页表、再遍历三级页表。

由于三级页表的结构是树(512叉树),因此我们可以使用循环或是递归的方式,可以参照freewalk一样使用递归去从根节点遍历到叶子节点。

在exec中加入指定的代码,让用户进程初始化后就可以调用vmprint

1
2
3
4
5
6
7
8
9
10
11
---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)
}

实现:循环

由于是三级页表的PTE打印,比较简单的实现就是通过三层嵌套循环将每一级的页表PTE打印出来。代码相对于递归的实现也是要多很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void vmprint(pagetable_t pagetable){ 
for(int i = 0; i < 512; i++){
//pagetable实际上就是一个存放pte的数组,类型为uint64
pte_t pte = pagetable[i];
if(pte & PTE_V){
uint64 pa2 = PTE2PA(pte);
printf("..%d: pte %p pa %p\n",i,pte,pa2);

for(int j = 0; j < 512; j++){
pagetable_t pagetable1 = (pagetable_t)pa2;
pte_t pte = pagetable1[j];

if(pte & PTE_V){
uint64 pa1 = PTE2PA(pte);
printf(".. ..%d: pte %p pa %p\n",j,pte,pa1);

for(int k = 0; k < 512; k++){

pagetable_t pagetable0 = (pagetable_t)pa1;
pte_t pte = pagetable0[k];

if(pte & PTE_V){
uint64 pa0 = PTE2PA(pte);
printf(".. .. ..%d: pte %p pa %p\n",k,pte,pa0);

}
}
}
}
}
}

递归

因为三级页表的数据结构与树结构十分相似,因此逻辑上也十分好实现。当遍历到叶子结点(第三级页表)时就结束递归即可。

递归的方式实现,由于vmprint的参数只设置了一个,因此需要添加一个称为fmtprint的函数通过添加level参数来控制格式输出。每次递归调用传递的就是pte转换后的下一级物理地址了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void fmtprint(pagetable_t pagetable,int level){
for(int i = 0; i < 512; i++){
//pagetable实际上就是一个存放pte的数组,类型为uint64
pte_t pte = pagetable[i];
if(pte & PTE_V){
uint64 pa = PTE2PA(pte);
switch (level)
{
case 2:
printf("..%d: pte %p pa %p\n",i,pte,pa);
fmtprint((pagetable_t)pa,level-1);
break;
case 1:
printf(".. ..%d: pte %p pa %p\n",i,pte,pa);
fmtprint((pagetable_t)pa,level-1);
break;
case 0:
printf(".. .. ..%d: pte %p pa %p\n",i,pte,pa);
}
}
}
}

void vmprint(pagetable_t pagetable){
printf("page table %p\n",pagetable);

fmtprint(pagetable,2);
}

Detect which pages have been accessed(hard)

实验要求

一些垃圾收集器(自动内存管理的一种形式)可以从哪些页面被访问(读或写)的信息中获益。在本部分实验中,您将向xv6添加一个新特性,该特性通过检查RISC-V页表中的访问位来检测该信息并将其报告给用户空间。当RISC-V硬件分页行走器解决TLB错误时,它会在PTE中标记这些位。

主要任务是实现pgaccess(),这是一个系统调用,用于报告哪些页面已被访问。系统调用有三个参数。首先,它使用第一个用户页的起始虚拟地址进行检查。其次,需要检查页数。最后,它将用户地址存入缓冲区,以将结果存储为位掩码(一种每页使用一位且第一页对应最低有效位的数据结构)。如果运行pgtbltest时pgaccess测试用例通过,将获得这部分实验的全部学分。

HINS:

  • 阅读user/pgtlbtest.c中的pgaccess_test()以查看如何使用pgaccess。

  • 在kernel/sysproc.c中实现sys_pgaccess()。

  • 使用argaddr()和argt()来解析参数,lab2对这两种方法有所使用。

  • 对于输出位掩码,在内核中存储一个临时缓冲区,并在填充正确的位后将其复制给用户(通过copyout())更容易。

  • kernel/vm.c中的walk()对于查找正确的pte非常有用。

  • 在kernel/riscv.h中定义PTE_A,即访问位。请参考RISC-V特权架构手册来确定其值。

  • 检查PTE_A是否设置后,请务必清除PTE_A。否则,将无法确定自pgaccess()最后一次调用以来是否访问了该页(即,将永远设置该位)。

  • vmprint()在调试页表时可能会派上用场。

实现

首先查看pgtbltest.c中的代码。pgaccess系统调用的参数分别为

  • 需要检查页的起始地址:这是一个虚拟地址,通过使用起始的虚拟地址配合walk函数(实现的是物理页的翻译过程),找到最后一级页表的PTE(物理页的PTE)。

  • 需要检查页的页数:从起始虚拟地址开始,以一个页的4096字节为单位作为一个索引(页数组),一段连续的虚拟页。

  • 位掩码:一个无符号整数(4字节,32位)每个位代表的就是该页表是否被访问过。

例如,在测试中有32个页。有1、2、30个页表被访问,那么abits对应的就是第2、3、31位设置为1,再通过判断我们就可以知晓这几个页被访问过了。

1
abits: 0100 0000 0000 0000 0000 0000 0000 0110

如下述测试案例中,使用了malloc申请了32个页的连续的虚拟页,因为页(page)是一个4096字节的数组。那么我们刚好可以使用4096*32长度的char(一个字节)的字符数组来表示32个页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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");
}

如果使用整数数组的话就是int buf[32*(PGSIZE/4)],每一页的起始位置就是buf[n * PGSIZE/4]

接着调用一次pgaccess(),但这时我们申请的几个页并没有使用(物理页已经存在,malloc调用了sbrk系统调用,创建了物理页)。

之后对创建的第1、2、30页进行读取并修改,此时这三个页就已经被访问了,那么相应的三个页的PTE_A的位也被设置为1了。

那么再调用一次pgaccess,通过abits为就可以知道那些页被访问过了。最后再判断abits的位,释放内存就结束测试了。

按照图3.3中的PTE的flag设置,将PTE_A设置为第6位。

1
2
---riscv.h---
#define PTE_A 1L<<6

实现sys_pgaccess系统调用

  1. 通过argaddr、argint,获得系统调用的前两个参数:第一个虚拟页的起始地址、页数目。

  2. 遍历页数目,通过walk将va+进程页表,获得最后一级页表的PTE。

  3. 判断获取的PTE,如果PTE_A位被设置,将abits变量对应的位设置为1,并将PTE_A清空。

  4. 由于这些页是连续的,需要获取下一个的虚拟页的起始地址为va+=PGSIZE

  5. 使用copyout将abits变量传回用户空间,注意copyout这个参数设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
---sysproc.c---
int
sys_pgaccess(void)
{

struct proc *p = myproc();
uint64 base,mask;
unsigned int abits = 0;
int len;

argaddr(0,&base);
argint(1,&len);

//base is virtual adddress,next page address = va + 4096
uint64 va = base;
for(int i = 0;i<len;i++){
pte_t* pte = walk(p->pagetable,va,0);
if(*pte & PTE_A){
abits |= 1<<i;
//clear the PTE_A flag
*pte &= (~PTE_A);
}
va += PGSIZE;
}
argaddr(2,&mask);
//copy abits to mask
if(copyout(p->pagetable, mask, (char *)&abits, sizeof(abits)) < 0)
return -1;

return 0;
}

遗留:现在有一个尚存的问题是通过buf对每一个页进行读取修改,翻看源码也没有看到是何时在通过虚拟地址(buf)访问访问到物理地址并且修改PTE的。

因为在实验的时候,是自己添加PTE_A这个位,所以我想的是在通过虚拟地址访问到物理地址并修改后,要自己去修改pte的PTE_A位。但是在我们完成sys_pgaccess函数之后运行就通过实验了。通过翻找vm.c以及sbrk调用我都没有找到在哪修改了pte,所以我将这个问题留到了最后。

猜测:可能是硬件更改的pte

实验结果

image.png