本周主要完成了2024-xv6-labsutilcow的五个实验。

1. util lab

1.1 sleep

这个算是最简单的实验了。按照hints一步步写即可。没有任何额外需要注意的地方。

1.2 pingpong

这个实验的主要难点是理解pipe函数的使用。
先查看 sys_pipe函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
sys_pipe(void)
{
uint64 fdarray; // user pointer to array of two integers
struct file *rf, *wf;
int fd0, fd1;
struct proc *p = myproc();

argaddr(0, &fdarray);
if(pipealloc(&rf, &wf) < 0){
...
}

...
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
...
}
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
...
}
...
}

再继续查看 pipealloc fdalloc copyout等相关函数的定义。
可见,pipe是把两个文件描述符分别传给了fdarray[0]和fdarray[1],且fdarray[0]只读,fdarray[1]只写。

而本题中,需要父->子 子->父两个管道。且对于父进程和子进程都需要读和写,最后代码如下:

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
int p1[2]; // 父进程->子进程
int p2[2]; // 子进程->父进程
pipe(p1);
pipe(p2);
char buf;
if(pid>0){
close(p1[0]); // 关闭p1读端
close(p2[1]); // 关闭p2写端
write(p1[1], "a", 1);
close(p1[1]);
read(p2[0],&buf,1);
close(p2[0]);
wait(0);
fprintf(2, "%d: received pong\n", getpid());
}
else{
close(p1[1]);
close(p2[0]);

read(p1[0],&buf,1);
close(p1[0]);
fprintf(2, "%d: received ping\n", getpid());
write(p2[1],"a",1);
close(p2[1]);
}

1.3 primes

这题更为考验 pipe的使用,而且要特别小心,需留意哪些文件描述符不会使用了并把他们都关闭。否则会导致资源耗尽,在大约37的地方输出乱码。

该题提取质数的流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Input: file descriptor
//Output: void
//print the "first" prime in the sequence filtered by last process, and past the
//left sequence to next process recursively.
primes(fd) :
if the sequence is empty do:
close(fd);
exit;
print the first number of the sequence;
fork;
pipe;
if is parent proc do:
pass the first number's multiples;
write the left numnbers to son proc via pipe;
close(fd);
close(pipe's write);
wait;
if is son proc do:
close(fd);
close(pipe's write);
primes(pipe's read);

exit;

1.4 find

实际上,find的写法跟 ls写法几乎相同,对于目录文件而言 ls是打印下面的所有文件,而 find只打印下面名字与第二个参数相同的文件。只是要注意到 find是递归查看目录文件,所以要额外处理”.”和”..”。

1.5 xargs

这题的关键是,如何处理 xargs|之前的部分的输出。

首先要查明,这些输出是被管道重定向到了 stdin里,而 stdin对应的 fd为0.

然后就是使用read将 stdin里的拼接在原argv后面。注意 ' ''\n'的情况,前者代表一个参数输入结束,后者不仅代表结束,还需要立即执行一次。

注意在 exec()中,argv末尾需以 0标志。

2. syscall lab

2.1 trace

这题其实理清流程即可,并不复杂:

trace的第一个参数(掩码)给 myproc()保存 ———— 所以 proc结构体需要新添加一个属性 mask来存。然后每当 myproc()执行系统调用时候,检查掩码中对于该系统调用对应的位是否为1(p->mask&(1<<num)),以此来决定是否打印该系统调用。

2.2 attack

这道题。。。。emmmm。。。说实话我现在都没有想通。

这道题思路很明显:既然新分配的内存保留了其先前使用的內容,那么attack也申请内存,在其中的一页中的32个偏移开始8字节就一定是 secrest

关键在于:哪一页?

说实话,我真是万策尽矣,无论是打印申请的每一页开头40字节,还是用前缀字符来比较(这个甚至诡异得一塌糊涂:attack申请后那一存着secret的页开头2字节是乱码),都一一失败了😭。

最后是在不得已,去github上找到了参考:申请32页的第16页。

以下是作者的原话:
不能理解!这个16是怎么来的??

(我也不能理解。。。)

3. pgtbl lab

第3章是我目前为止xv6中做过的最痛苦的实验了😭,我真花了两三天时间做这个。尤其是最后一个超级块的部分,我搞了约14个小时,才过测试。。。

3.1 ugetpid

这个题目的难点。。。在于看不懂题目,找不到该在哪里写什么(bushi)。

但其实也很简单:在用户空间里拿一页空间 USYSCALLusyscallusyscall来存一些东西(这里是 getpid()proc定义里添加它的地址,allocproc freeproc proc_pagetable proc_freepagetable这些与进程内存分配释放的函数里面也类似地加上处理 USYSCALL的部分即可。

但是有一点要注意:关于 USYSCALL页”user can access but read only”.

3.2 vmprint

这个的关键点在于,搞清楚虚拟页和对应的三级页表。

  • 如果PTE在0级页表上,则对应的虚拟页相对于初始虚拟页的偏移是在30~38位上;
  • 如果PTE在1级页表上,则对应的虚拟页相对于初始虚拟页的偏移是在21~29位上;
  • 如果PTE在2级页表上,则对应的虚拟页相对于初始虚拟页的偏移是在12~20位上。

(PTE指的是有效的,且不表示下一级页表的pte项)

3.3 super block

太困难了!太困难了!😭

我最后甚至也是钻了空子(我留了10个超级块,而测试最多用8个超级块)才过的。

题目的思路很清晰:留几个”super block”使得申请大内存空间时不是给他很多个小页而是给他几个超级块。那么,只要在管理分配页的数据结构中添加超级页并给它们标志,并在处理页的申请释放的函数中类似地加上相应处理逻辑即可。

但是!!!有一些坑!

  1. 当地址未对齐时,普通页可以直接 PGROUNDUP对齐。但超级页不能类比!如果直接SUPERPGROUNDUP再分配超级块的话,会导致内存地址空间出现“空隙”,panic(“uvmcopy: page not present”)。所以为了防止这个问题,要先通过分配普通块的方式进行对齐。
  2. 超级页必须是在第1级页表上的pte表示,而不是普通页的第0级。因为超级页2MB,普通页4KB,一个第1级页表上的pte表示512个第0级pte————第1级的pte表示2MB

4. trap lab

4.1 backtrace

这题也比较简单,照着这个笔记上关于栈帧布局的示意图写代码并打印即可。

4.2 alarm

虽是 题,但我觉得比 pgtbl那道 题超级块简单得多。。。

事实上,跟着hints一步步做就好,只是要注意:在进入handler函数时,时钟计数并不会自然停止,我们该在 proc额外设置一个属性来判断当前是否处于handler中。

而且,为了恢复上下文,我们还应该设一个结构体属性作为备份,来存进入handler之前的trapframe的所有内容。

5. cow lab

这个实验也挺。。。先是filetest死活过不了,花了我几个小时找bug,然后把bug一改,嘿!您猜怎么着?threetest也过不了了🤣。
最后找了两个多小时才找到原来是 kfree这边的问题。

做法其实也是跟着hints一步步来,(注意trap要处理的是 r_scause()==15的情景)只是还是有坑:

由于我把减引用计数的逻辑与kfree写一起的,
所以我天真的以为只需要开始减引用计数时和末尾更新 freelist才需要加锁,而中间是不用锁的。然后 kerneltrap给我狠狠上了一课。😂

最后还得别忘了在 usetrap添加处理虚拟地址超出最大值时杀死进程的逻辑,否则 usertests -q过不了。🤣

6. 感谢

  1. MIT的cs6.828以及xv6-labs的设计者们,感谢他们提供如此优质的项目。虽然写的时候很痛苦😂但确实我感到自己对于C语言和操作系统的理解与认识有所深入。
  2. siriusyaoz,在做实验的时候,你的关于这个实验的代码给予了我很多启发🥹。