MIT6.S081-Lab1
之前写过xv6的安装,和一些课程先导,有挺多学习资料的,在第一篇文章,本来还想弄一个系列,但是我又懒,关键要做lab,那就先写lab吧,顺带一提xv6的英文文档阅读起来还是有一些困难的,甚至咱的作业也是没看懂,但是lab重要。
先贴上lab的地址
这边说一个问题,就是这个课每年的课程地址都在改变如果你使用的和我一样是2020年的地址,那么你就会遇到一个问题,2020年的lab仓库里的代码出现bug的解决方案到如今2023年已经无法使用了,所以办法就是切换新的lab。下面的lab地址我已经换成了2022年的了。
稍微提一下这个bug,在2020年的安装指导里提到运行make qemu之后会卡在编译的最后环节。
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
然后解决方案是换一个更低版本的包,but现在这个包已经被删除了,无独有偶找到一篇知乎的文章,提到了lab地址就想到了,更新后的lab代码。
Lab: Xv6 and Unix utilities
稍微翻译一下lab。。。
这个实验会帮助你熟悉xv6和它的系统调用。
启动xv6(简单)
上一次已经启动过了,我以为上课要先给他启动起来,结果第一节课的实验的第一题居然是启动。
很简单就是说如果没有Athena(大概是MIT的内部服务器,可以直接用)要在自己电脑上装的话就看lab tools page来安装。
然后是一堆关于Athena的使用直接忽略。
下一段就是说要先clone实验(在github上)
1 |
|
这边我看了一下应该是有两个分支,一个分支是origin,另一个是util,如果我们不做任何修改的话目前origin和util现在是一样的,我们需要切换到util进行代码编写实验修改xv6。
使用git status
查看当前分支。
切换分支的命令如上,其次还提到了下载的lab代码xv6-labs-2020 repository和book’s xv6-riscv里的略有不同。主要是增加了一些文件,运行git log
查看历史记录。稍微看了一下,这个历史修改记录有亿点点多。
然后说了一下,这些代码后续提交什么的需要使用Git,推了基本Git的帮助手册,使用Git的书,这里就不看了,还是英文的,大可不必再烧我的脑细胞。提到了上述切换分支的util分支包含了契合于实验版本的xv6,Git可以跟踪修改如果完成了练习,可以使用如下命令提交修改。
1 |
|
然后说,如果要查看修改可以使用git diff命令,可以使用它来显示你最后一次修改的差异。使用git diff origin/util
可以显示你的代码与原始版本的xv6代码的差异,这里提到了origin分支是原始xv6代码。
然后如果你按照上述安装文档,安装完qemu和编译所需的运行环境时,就可以直接编译运行xv6了。
使用make qemu
命令就开始编译运行了。
这里粘贴一下运行正常显示的代码。
1 |
|
大概就这样,最下面会有一个$符,当成是shell,然后输入ls命令得出类似于如下的结果就证明没有问题。
1 |
|
下面是说了一下有很多文件可以执行包括mkfs,大多数程序都可以运行,包括刚刚的ls。
然后提了一下xv6没有ps这个命令,你可以使用Ctrl-p来查看进程信息,如果你现在运行,应该可以看到init和sh。
退出qemu使用Ctrl-a x
。
最后说了一下可以运行make grade来查看测试结果,提交是运行make handin,咱大概是不需要提交的。
发现了个bug卡了很久,但是解决了,问题是要看最新的lab指导。
顺带提一嘴,老师上课的时候使用了make clean命令,清除了编译缓存,为了展示一下编译过程。
sleep(简单)
编写一个UNIX程序为xv6实现sleep功能,你的sleep功能应该实现暂停用户指定的tick(滴答)数,一个tick是xv6内核定义的时间概念,也就是计时器两次停顿的中间时间,你的代码应该写在/user/sleep.c。
一些提示:
- 开始编程前请先阅读xv6 book的第一章。(蒸汽牛奶:关于xv6的介绍书,已经更新到了第三个版本。建议下载最新版,或者实验对用的版本)。
- 阅读其他位于/user目录的代码(例如,user/echo.c,user/grep.c和user/rm.c),注意观察命令行参数对程序的传递。
- 如果用户没有给sleep传递参数,程序应该打印错误信息。
- 命令行参数是以字符串的形式传递,你可以使用
atoi
(详见 user/ulib.c).将其转换成一个整数(integer)。 - 使用 系统调用 sleep。
- 阅读xv6的内核代码kernel/sysproc.c实现系统调用sleep的代码(寻找sys_sleep),user/user.h中对于可供用户程序调用的sleep的C的定义,还有user/usys.S中sleep从用户代码跳转到内核的汇编代码实现。
- main函数需要在结束时调用exit(0)。
- 将你的sleep程序添加到UPROGS在Makefile中,这样make qemu就会编译你的源代码并且可以在shell中运行。
- 阅读Kernighan and Ritchie’s book The C programming language (second edition) (K&R)来学习C。
运行程序:
1 |
|
如果运行结果如上的话,那么程序应该就是正确的,运行make grade可以检查程序是否正真通过了测试。
注意make grade会运行所有测试,如果只要运行一项测试就运行下面的任意一个指令(效果一样)。
1 |
|
结果:
这题很简单哈,但是我不会。。。于是我就偷看了答案,很久没看C语言了,我真的会谢,很简单的题目看了很久。
1 |
|
解析:
关于头文件,貌似类似于echo.c和rm.c都是这个头文件,我看其他的答案有少了一条的,具体是否调用也不清楚。其次错误输出用fprintf重定向到2也就是错误输出。这边提一下因为xv6的书里提到了,每个程旭启动的时候默认保持最少三个文件描述符,即0 stdin,1stdout,2stderr。
还有几个看不懂的地方,首先是argc这个经查阅代表的是传递过来的参数个数,最少是两个sleep num,包括sleep和睡眠时间。再者使用atoi转换argv[1]。
检验:
1 |
|
再Makefile里面找到UPROGS在最下面添加编写的sleep,然后make qemu时就可以编译并且在命令行运行。
1 |
|
运行命令的原理
很纳闷为什么参数是这样传递的,于是又翻了翻xv6 book,shell调用exec来代表用户执行程序。一个主循环使用getcmd循环读入用户输入,然后调用fork创建一个父进程的副本,父进程调用wait等待子进程结束,子进程执行命令。
参数传递:
关于调用主函数的参数,我的理解为传给runcmd的是两个参数,在runcmd调用exec,如果exec执行成功,echo将替换掉runcmd,我理解为exec传递给sleep的参数为,第一个参数是参数个数,即argc=2,如果小于2就证明没有传递给sleep参数,第二个就是sleep和time,使用argv[1]是因为忽略第一个参数sleep。再提一嘴就是弹出的报错的意思是sleep的使用方法,usage又规范用法的意思。
fork和exec:
再提一嘴为什么要先fork再exec,类似于书上win32里面有CreateProcess这个API,实际上百度一番发现这个方法并不是很适合多线程即现在的环境,也不是很推崇使用,至于现在还存在于Linux中是因为保留的Unix的部分,至于以前为什么用是因为以前的内存很小甚至是单线程,使用exec会干掉原来的shell,那么当exec运行完成之后再启动shell就啥也没了,没了交互。再者还提到一个观点就是对子进程环境的修改,而不影响父进程,比如修改子进程的文件描述符标准输出到文件,但是又不想影响到父进程,于是就出现了fork,fork不适用于现在的环境,特别是进程内存及其他很大的时候,fork一个一模一样的进程很消耗资源。
pingpong(简单)
编写一个程序使用Unix系统调用在两个进程间使用管道“ping-pong”一个字节,每个方向各有一个。父进程向子进程发送一个byte,子进程打印“<pid>:received ping”,pid时进程id,然后再写一个字节到通向父进程的管道中,再退出。父进程收到子进程发送的byte后,打印“<pid>: received pong”,然后退出。文件位于user/pingpong.c。
一些提示:
- 使用pipe来创建一个管道。
- 使用fork来创建一个子进程。
- 使用read来从管道读取,使用write来写入管道。
- 使用getpid来获取调用进程的pid。
- 将程序添加到Makefile的UPROGS中。
- xv6上用户程序可调用的库函数的表位于user/user.h中,源代码在user/ulib.c, user/printf.c, 和user/umalloc.c中。
运行程序:
1 |
|
结果:
1 |
|
解析:
一个注意点子进程和父进程都需要exit(0)。(网上看到的)
调用函数创建管道需要在主函数内创建。
冒号后面还有个空格。。。
然后wait(0),主进程要等待子进程结束再printf,不然输出会绞在一起。
user.h
由最后一个提示可以得到user.h包含了用户程序可调用的库函数,打开可以看到里面包含了系统调用和ulib.c。
1 |
|
fork
关于fork的一个点是fork在父进程和子进程都会有返回,在父进程中返回子进程的pid,在子进程中返回0。
检验:
1 |
|
primes(适中)/(困难)
(咱就是说有时候自己看不懂Google翻译还是挺准的)
使用管道编写prime sieve的并发版本。这个想法源自于Unix的管道发明者Doug Mcllroy。本页中的图片和周围文字说明了该如何操作。文件应位于user/primes.c。
你的目标是使用pipe和fork来创建设置管道。第一个进程将数字2到35输入管道。对于每个质数,将创建一个进程,进程通过管道从其左邻居读取并通过管道写入其右邻居。由于xv6的文件描述符和进程数量限制,所以数字是最大35。
一些提示:
- 注意关闭进程不需要的文件描述符,因为xv6将在没有到达数字35之前耗尽资源。
- 一旦第一个进程到达35,它需要等待所有管道线路关闭,包括所有子进程,子进程的子进程等。所以primes的主函数只有在在所有的输出都已经被打印之后,所有的进程都退出后才能结束。
- 如果管道的写端口已经关闭,read会返回0(读完之后)。
- 最简单的方式是直接写32-bit(4-byte)的int传给管道,而不是使用标准ASCII I/O。
- 你只有在管道线路需要的时候才能创建进程。
- 将文件添加到Makefile的UPROGS。
运行程序:
1 |
|
结果:
1 |
|
解析:
提示说到最简单的方式是写入4-byte的数据,于是read和write的数据大小。
不知道是我基础太薄弱还是智商有限哈,看了半天都没看懂,直接复制粘贴别人的代码开始做注解了,还有两个别人写的代码超级长,吃不消,定义了好多东西。。。
首先第一个进程将2-35全部写入管道,然后先打印第一个,每次传入的第一个数字一定是质数,然后将数据对n取余,将不能够整除n的数字进入下一轮,根据文档中的图片。
第一轮去除4,6,8,10…
第二轮去除9…
一个问题是需要关闭文件描述符和关闭内存释放资源。
检验:
1 |
|
find(适中)
编写一个简单的UNIX查找程序:在文件树中查找指定名字的所有文件。你的C文件应该被放在user/find.c。
一些提示:
阅读user/ls.c来学习如何读取目录。
利用递归来允许find访问子目录。
不要递归进入“.”和“..”。
你需要使用C字符串,阅读K&R的The C book,例如5.5。