文章目录
- 前言
- 其他篇章
- 参考链接
- 0. 前置环境
- 1. Speed up system calls (easy)
- 1.1 简单分析
- 1.2 映射
- 1.3 页分配
- 1.4 页释放
- 1.5 测试
- 2. Print a page table (easy)
- 2.1 简单分析
- 2.2 实现
- 2.3 测试
- 3. Detect which pages have been accessed (hard)
- 3.1 简单分析
- 3.2 实现
- 3.2.1 获取参数
- 3.2.2 传出参数
- 3.2.3 定义PTE_A
- 3.2.4 实现主体逻辑
- 3.3 测试
- 测试
前言
这一个Lab是往年叫苦声最大的、最难的一个lab,不过今年显然简化了不少,换掉了Task,其间意义见仁见智吧。
其他篇章
环境搭建
Lab1: Utilities
Lab2: System calls
Lab3: Page tables
参考链接
官网链接
xv6手册链接,这个挺重要的,建议做lab之前最好读一读。
xv6手册中文版,这是几位先辈们的辛勤奉献来的呀!再习惯英文文档阅读我还是更喜欢中文一点,开源无敌!
OSTEP,对OS不熟悉的同学做之前可以看一下这本经典书籍,写得很好,也有中文版实体书。
官方文档
0. 前置环境
如果你和我操作步骤一直一样,那就可以在VS的远程仓库里找到分支base/pgtbl
分支,选中
打开分支管理器(Alt
->G
->M
),右键pgtbl,取消设置上游分支,然后右键推送,显示成功推送到origin,这样就成功了
然后在wsl里的对应文件夹下,git pull
后git checkout pgtbl
,整体配置完成:
1. Speed up system calls (easy)
1.1 简单分析
上一个Lab我们实现了两个系统调用,从中可以认识到系统调用涉及到用户态与内核态的切换,自然也就涉及到了各种参数传来传去的问题。本Lab开篇就介绍了许多操作系统都通过维护一个read-only的共享内存区去实现内核态与用户态资源的共享,免去了某些资源交换的过程,从而提升系统调用的效率。
介绍完后,本Task要求我们在xv6中为getpid
实现这种功能,我们知道操作系统通过页表去管理内存,而它告诉我们每个进程创建时都会映射到一个USYSCALL,这玩意是个VA,也就是Virtual Address,这应该就是我们的共享区域的起始地址,打开他提到的文件看一看:
可以看到,这个USYSCALL是由TRAPFRAME往前偏移一页算出来的,而TRAPFRAME又是由TRAMPOLINE偏移出来的,TRAMPOLINE页相当于在VA的最后一页上,里面映射了一些内核的指令,用于陷入内核,而TRAPFRAME页则负责保存进程相关的一些数据。此外,可以注意到这个地方有一个条件编译,这个是在Makefile里编译启用的,我们不用手动宏定义,或者看着不爽先宏定义一下后面撤掉也行。结构体里面目前就一个pid,后面看看用不用得上。
1.2 映射
然后看一看Hint:
这在提示我们怎么去做USYSCALL这个映射,我们首先看一下proc.c
扫一眼就可以看出,这里做的是进程向trampoline page
和trapframe page
的映射,在申请资源后,每次map
都需要检查一下是否成功,不成功就得释放之前申请过的资源以及映射过的页。因此我们可以往里面添加这样一些代码:
#ifdef LAB_PGTBL // 模仿着memlayout.h加上条件编译
// 映射
if (mappages(pagetable, USYSCALL, PGSIZE,
// TODO: 还差后面两个参数
) < 0) {
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
#endif
看一下倒数第二个参数,我们可以发现这个trapframe就是存在proc里的一个指针而已,因此我们也在proc.h
加上usyscall
指针的定义:
#ifdef LAB_PGTBL
struct usyscall* usyscall;
#endif
然后看一下mappages
函数的最后一个参数,最后一个参数代表了所谓的PTE的值,标记了分页的一些状态,打开定义位置,我们可以看到这里定义了五个宏:
关于这些标志位的解释xv6 book里有,我之前放那个中文的链接是基于x86的,和现在的RISC-V在这里有一点不一样,所以我这里就放原文了:
可以看到,这五个标志位分别标记了是否有效、可读、可写、可执行(将页标记为指令,像之前说的trampoline page
,里面就放的一些内核的指令,因此我们看到它被标记上了PTE_X
)、用户可用,我们的这个共享页需要可读且用户态与内核态都可以访问,因此我们需要将它设置为PTE_R | PTE_U
。
据此我们依葫芦画瓢照着映射我们的usyscall page
就行:
...
#ifdef LAB_PGTBL // 模仿着memlayout.h加上条件编译
// 映射到USYSCALL
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;
}
#endif
...
1.3 页分配
没啥好说的,找到allocproc
函数照猫画虎就行,只是别忘了给pid赋值
#ifdef LAB_PGTBL
if ((p->usyscall = (struct usyscall*)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid; // 别忘了给usyscall的pid赋值
#endif
1.4 页释放
往freeproc
里释放usyscall
#ifdef LAB_PGTBL
if (p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
#endif
记得我们前面初始化的时候映射失败需要调用unmap
去取消映射吗?正常运行完毕自然也要去做这个事情,做这个事情的函数就在下面那个proc_freepagetable
里,F12
打开,加上去:
#ifdef LAB_PGTBL
uvmunmap(pagetable, USYSCALL, 1, 0);
#endif
就此就搞定了
1.5 测试
推送后make qemu
,本来是很稀松平常的事情,结果我一直报这个错:
make: *** 没有规则可制作目标“kernel/sysinfo.h”,由“kernel/sysproc.o” 需求。 停止。
然后我又是回退还原又是各种各样的操作,都依然报这个错,网上也一直找不到别人吐槽这个事情,最后我make clean
了一下,成功了,,,这玩意卡我一个多小时你敢信?
然后输入pgtbltest
,看到ugetpid_test那里显示OK就行:
跑一下 ./grade-lab-pgtbl ugetpid
:
2. Print a page table (easy)
2.1 简单分析
这个task要我们写一个打印页表的函数,也比较简单:
初步阅读上文可以简单提炼出需求:我们需要在kernel/vm.c
中定义一个名为vmprintf()
的函数,接受并按格式打印一个pagetable_t
类型的参数,然后在exec.c
中return argc
插入if(p->pid==1) vmprint(p->pagetable)
语句用来打印第一个进程的page table,读到这里,我们顺手给他塞进去:
然后看看打印格式:
The first line displays the argument to vmprint. After that there is a line for each PTE, including PTEs that refer to page-table pages deeper in the tree. Each PTE line is indented by a number of " …" that indicates its depth in the tree. Each PTE line shows the PTE index in its page-table page, the pte bits, and the physical address extracted from the PTE. Don’t print PTEs that are not valid. In the above example, the top-level page-table page has mappings for entries 0 and 255. The next level down for entry 0 has only index 0 mapped, and the bottom-level for that index 0 has entries 0, 1, and 2 mapped.
可以看到,第一行打印了vmprint
的参数,后面各行展示了页表所属下方的条目,那么问题来了——我们怎么知道页表下面有哪些页面呢?参照The function freewalk may be inspirational. 因此我们可以看一下这个函数:
打开pagetable_t
的定义发现这其实就是个指针型别,看注释这里是用了9位用来表示子页表,因此它遍历了512位,寻址后判定对期望的标志位的页面使用PTE2PA
截断了低10位和高2位,然后继续递归进入执行逻辑,可以看出这是个DFS。值得注意的一点是,标志位限定了不可读、不可写、不可执行的页面才进入下一步递归,因为这意味着这是个间接层,不记载内容,只作为多级页表的一级。
2.2 实现
分析清楚后我们就可以写我们的函数了,由于我们要根据深度打印.
,因此我们可以给参数传入一个深度的参数,我们可以为这个递归函数设立一个helper函数,对外接口就只暴露调用helper的vmprint
本身,避免污染。
void
vmprint_dfs(pagetable_t pagetable, uint depth)
{
static char* prefix[] = {
[1] = "..",
".. ..",
".. .. .."
};
if (depth > 3) {
panic("vmprint_dfs: depth > 3");
return;
}
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if (pte & PTE_V) {
pte_t child = PTE2PA(pte);
printf("%s%d: pte %p pa %p\n", prefix[depth], i, pte, child);
if (child & (PTE_R | PTE_W | PTE_X) == 0) {
vmprint_dfs((pagetable_t)child, depth + 1);
}
}
}
}
void
vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
vmprint_dfs(pagetable, 1);
}
然后在defs.h
中暴露出接口:
到此就基本搞定了,看一看:
2.3 测试
运行一下测试脚本./grade-lab-pgtbl pte printout
,通过:
3. Detect which pages have been accessed (hard)
3.1 简单分析
首先lab介绍了一下标记page是否被访问过(accessed)是比较有用的一个信息,比如对GC有用,这个位维护在一些位里,由RISC-V的硬件页遍历器(hardware page walker)去维护这些位。我们要做的就是检查这些页,并返回给用户态。
具体而言,我们需要实现一个名为pgaccess
的系统调用,用于报告哪些页被访问过,它接受三个参数:
- 待检查的第一个用户页的起始VA
- 待检查页面的数量
- 存储结果(被访问了的页面号)用的
bitmap
第一个Hint还告诉我们可以从user/pgtlbtest.c
中的pgaccess_test()
看一看pgaccess
是怎么用的:
可以看到,pgaccess
应当在失败时返回一个-1,第1、2、30页被访问过了,因此最后结果abits
的对应位就被置为了1。
3.2 实现
理清楚这些东西,实现起来就很简单了,上个lab告诉了我们syscall实现的步骤,不过这次我们只用写实现就行了,不用关注那些繁文缛节的事情。
3.2.1 获取参数
依赖之前的经验获取参数,不多说
uint64 va; // 待检测页表起始地址
int num_pages; // 待检测页表的页数
uint64 access_mask; // 记录检测结果的掩码
// 从用户栈中获取参数
argaddr(0, &va);
argint(1, &num_pages);
argaddr(2, &access_mask);
3.2.2 传出参数
For the output bitmask, it’s easier to store a temporary buffer in the kernel and copy it to the user (via copyout()) after filling it with the right bits. 提示我们可以用一个中间变量把mask存起来由此可以完善我们的实现:
int
sys_pgaccess(void)
{
uint64 va; // 待检测页表起始地址
int num_pages; // 待检测页表的页数
uint64 access_mask; // 记录检测结果掩码的地址
// 从用户栈中获取参数
argaddr(0, &va);
argint(1, &num_pages);
argaddr(2, &access_mask);
if (num_pages <= 0 || num_pages > 512)
{
return -1;
}
uint mask = 0;
// TODO
copyout(myproc()->pagetable, access_mask, (char*)&mask, sizeof(mask));
return 0;
}
3.2.3 定义PTE_A
刚才说了,我们实际上是用一个accessed位去记录信息的,这个位同样也保存在PTE中,题中要求我们去在riscv.h
中定义一下这个位,那么问题来了,这个位定义成多少呢?
查阅risc-v手册可以看到,risc-v中将PTE_A放在了第六位,因此我们在riscv.h
中加入:
#define PTE_A (1L << 6) // accessed
或者干脆全定义了算了()
3.2.4 实现主体逻辑
然后就比较简单了,我们遍历页表,利用walk
获取pte,然后对PTE_A
置位的页复位,并把页码放在mask里:
int
sys_pgaccess(void)
{
struct proc* p = myproc();
uint64 va; // 待检测页表起始地址
int num_pages; // 待检测页表的页数
uint64 access_mask; // 记录检测结果掩码的地址
// 从用户栈中获取参数
argaddr(0, &va);
argint(1, &num_pages);
argaddr(2, &access_mask);
if (num_pages <= 0 || num_pages > 512)
{
return -1;
}
uint mask = 0;
// 遍历页表
for (int i = 0; i < num_pages; i++)
{
pte_t* pte = walk(p->pagetable, va + i * PGSIZE, 0);
if (pte && (*pte & PTE_V) && (*pte & PTE_A))
{
*pte &= ~PTE_A; // 清除访问位
mask |= (1 << i);
}
}
// 将检测结果写入用户栈
copyout(p->pagetable, access_mask, (char*)&mask, sizeof(mask));
return 0;
}
3.3 测试
make qemu
后pgtbltest
,测试成功:
./grade-lab-pgtbl pgaccess
一下:
测试
最后添加time.txt
和answers-pgtbl.txt
,跑一下make grade
,通过(话说不知道那个Test time为什么卡老半天):