lab3

地址空间

  • trampoline被映射两次用以跳转和跳出内核。

  • 每个cpu拥有各自的kernel stack,相当于存在各自的kernel线程

  • 内核物理地址与虚拟地址相同。
  • 设备被映射到低地址

页表

页表保存于内存中,用于将虚拟内存转化为对应的物理内存。

虚拟内存

  • 在xv6里,只使用了39个bit

  • Offset:12bit,指向4096字节的某一个,此处虚拟内存与物理内存值相同。

  • L2,L1,L0,分别使用9bit索引表单

  • SATP寄存器存储页表的物理地址(即L2第0号索引)。

  • 在切换SATP寄存器后要对TLB进行刷新,否则会索引到错误地址

物理内存:

  • riscv支持使用56bit索引物理内存
  • Offset:12bit,指向4096字节的某一个,此处虚拟内存与物理内存值相同。

  • PPN由L0获得,占44bit。

使用三级页表的优点:

  • 极大节省内存空间的同时又不太影响性能。
  • 硬件可以帮助我们完成地址翻译,但是我们还是需要walk函数转换地址实现内核功能。
  • 因为内核虚拟地址和实际的物理地址是相同的,所以通过walk

页表缓存

处理器都会对于最近使用过的虚拟地址的翻译结果有缓存。这个缓存被称为:Translation Lookside Buffer(TLB)。

TLB在切换页表后要清空,否则地址翻译可能会出错。

Speed up system calls(2021)

注意21年前的lab是没有这个实验的。

主要是要理解创建进程时的内存映射过程,这里搞了很久,最后看题解发现其实很简单…来看一下吧

lab要求是加快getpid()系统调用的速度,那么该怎么加快呢?我们知道,pid在进程的运行周期是不会改变的,因此可以将内存

原:系统调用->syscall()->将参数从用户虚拟地址传送到内核虚拟地址->获取pid->将返回值从内核虚拟地址传送到用户虚拟地址

新:创建进程->将pid写入共享内存->用户调用普通函数(位于ulib.c)即可读取(不必陷入内核)

memlayout.h已经定义了usyscall在虚拟内存中的位置和数据结构

1
2
3
4
5
#define USYSCALL (TRAPFRAME - PGSIZE)

struct usyscall {
int pid; // Process ID
};

proc.h将usyscall添加进结构体,因为进程要监控虚拟内存以及回收虚拟内存

1
struct usyscall *usyscall;

proc.c先分配一段内存用于存放usyscall,然后赋值。

注意不能写在p->pagetable = proc_pagetable(p);前。因为在此函数内,我们需要将分配的内存映射到用户空间,包括我们实现的usyscall

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
static struct proc*
allocproc(void)
{
found:
p->pid = allocpid();
p->state = USED;

// 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; //赋值

// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
}

让我们来编辑proc_pagetable(p),可以仿照前面的代码,这里我将USYSCALL映射写在最后,如果映射失败,也需要将前面的成功映射去除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

当进程结束时,我们需要取消映射,修改proc_freepagetable,增加uvmunmap(pagetable, USYSCALL, 1, 0);

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

当我们去除了映射,就应当free掉内存,在freeproc释放掉usyscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;

if(p->usyscall) //free usyscall
kfree((void*)p->usyscall);
p->usyscall = 0;

if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
}

那么我们就完成这个lab,主要是加强了我们对内存页分配流程的理解。

首先要分配一页内存(4096kb),内核填充数据,将内存映射到用户的虚拟内存,此时操作系统和进程虽然有不同的虚拟地址,却能访问相同的数据。

进程结束后,内核取消映射(用户将无法通过虚拟地址访问数据),然后操作系统free内存,腾出空间给下一进程使用。

lab要求是我们打印第一个进程(pid = 1)的虚拟内存空间地址,要求按三层页表结构逐层打印

首先,这是一个内核函数,我们需要在defs.h里填写该函数

1
2
// vm.c
void vmprint(pagetable_t);

紧接着,我们在exec.c的对应位置添加函数

1
2
3
4
proc_freepagetable(oldpagetable, oldsz);
if(p->pid==1)
vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)

这里借鉴freewalk()的递归调用,但是由于我们需要记录层数,在内核声明全局变量又不可取,因此这里采用包装函数,在外函数记录层数。

判断层数:

L1、L2:效且读写执行权限,程序显然不能修改虚拟内存的映射

L0:效且读写权限,L0属于真实地址,程序需要读写(执行)地址来实现操作。

若有效位PTE_V没有设置,则该页表不存在。

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 vm_print_child(pagetable_t pagetable, int level)
{

for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
uint64 child = PTE2PA(pte);

if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
//有效且无读、写或执行权限
for (int l = 0; l < level; l++) {
printf(" ..");
}
printf("%d: pte %p pa %p\n",i, pte, child);
vm_print_child((pagetable_t)child, level+1);

}else if(pte & PTE_V) //到达第三级页表,有效且有读写权限为分配好的页,打印树
{
for (int l = 0; l < level; l++) {
printf(" ..");
}
printf("%d: pte %p pa %p\n",i, pte, child);
}

}
}

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

Detect which pages have been accessed

这个lab要求我们判断多个连续内存页中哪个内存页会被修改,返回掩码。

当该内存被访问时,硬件分页步行器会在PTE标记PTE_A,表示该页被访问到。

如图,PTE_A在第6位,我们修改riscv.h文件,定义宏

1
2
#define PTE_A (1L << 6)
#define MAX_SCAN_PAGE 32 //int为32位

紧接着我们利用argaddrargint读入参数,使用walk函数(defs.h未定义,需要extern)遍历内存地址,然后对返回的pte进行PTE_A判断,判断后要将PTE_A置0,防止影响下次调用。

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
32
33
#ifdef LAB_PGTBL
extern pte_t * walk(pagetable_t, uint64, int); //别忘了extern
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
int n;
uint64 buf, addr;
int mask = 0;
if(argaddr(0, &buf) < 0 ||argint(1, &n) < 0 || argaddr(2, &addr) < 0)
return -1;
if(n <= 0 || n > MAX_SCAN_PAGE)
return -1;

pte_t *pte;
struct proc *p = myproc();
for(int i = 0; i < n; i ++)
{
if((pte = walk(p->pagetable, buf + i*PGSIZE, 0))==0)
panic("pgaccess: walk failed");
if(*pte&PTE_A)
{
mask |= (1 << i);
*pte &= (~PTE_A);
}
}

if(copyout(p->pagetable, addr, (char *)&mask, sizeof(int)) < 0)
return -1;

return 0;
}
#endif

PTE_A标志位的作用:通过PTE_A可以判断是否被经常访问,否则可以放入磁盘节省珍贵的物理内存,内存交换

Simplify

//TODO