MIT6.S081实验一,代码都有详解,包含一些刚学习踩的坑
Lab1- Unix utilities
主要是配置环境,然后在用户空间实现一些功能函数。
环境配置
- 个人虚拟机:ubuntu 20.04 LTS;
- 配置一些必要的工具链;
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
sudo apt-get remove qemu-system-misc
sudo apt-get install qemu-system-misc=1:4.2-3ubuntu6
tips:其它操作系统可以看官方给的配置文档:link
3.将实验的xv6系统克隆到虚拟机上
git clone git://g.csail.mit.edu/xv6-labs-2020
cd xv6-labs-2020
git checkout util
make qemu
4.退出qemu
刚开始也没弄明白怎么退出,后面查了发现要先按CTRL+a,然后松开按键,再在命令行里输入x,就退出了。或者直接kill后台的qemu也行。
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$ QEMU: Terminated //退出成功
sleep函数实现
流程:
- 在user/目录下,创建一个sleep.c文件;
- 然后调用系统函数sleep和输入参数来实现休眠;
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
//命令格式: sleep time
int main(int argc, char *argv[]){
//判断参数数目,sleep、time两个
if(argc != 2){
fprintf(2,"Usage:%s <num> \n", argv[0]);
exit(1);
}
//将第二个参数time 转换为整形,调用sleep函数
int ticks = atoi(argv[1]);
int ret = sleep(ticks);
exit(ret);
}
- 在Makefile文件里面UPROGS 添加 $U/_sleep\;
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
...
$U/_sleep\ //添加进Makefile
- 直接重新编译make qemu,或者用测试脚本测试,我这里就放一下用脚本测试的方法和结果;
jimmy@ubuntu:~/xv6-labs-2020$ ./grade-lab-util sleep
make: 'kernel/kernel' is up to date.
== Test sleep, no arguments == sleep, no arguments: OK (0.8s)
== Test sleep, returns == sleep, returns: OK (1.0s)
== Test sleep, makes syscall == sleep, makes syscall: OK (1.1s)
pingpong函数
1.让父进程给子进程传一个byte,然后子进程收到,就打印一个received ping
2.子进程再给父进程传一个byte,然后父进程收到,就打印一个received pong
这个实验主要是熟悉fork()、pipe()、read()、write()、getpid() 等函数的使用方法
流程:
- 在user/目录下,创建一个pingpong.c文件;
- 编写代码
#include "kernel/types.h"
#include "user/user.h"
//管道需要一个数组,来存放写入端(第二个元素)和读取端(第一个元素)
int main()
{
//自定义一个父子进程间发送的byte
char buf[] = {'a'};
//定义两个管道数组
int p1[2];
int p2[2];
int pid;
//创建管道
pipe(p1); //父进程发,子进程读
pipe(p2); //子进程发,父进程读
//注意这里的顺序,一定要先创建好管道在创建子进程
//因为子进程要copy父进程的所有内容,如果后创建管道会出现问题
int ret = fork();
if(ret == 0){ //子进程
//获取当前的pid(用来确认打印出来是子进程的pid)
pid = getpid();
//关闭p1的写入端、p2的读取端(这两个不需要用到,最好关闭)
close(p1[1]);
close(p2[0]);
//如果从p1管道读到了内容,长度为1,就说明收到了父进程的byte
if(read(p1[0],buf,1) == 1){
printf("%d: received ping\n", pid);
}
//向p2管道写入一个byte
write(p2[1], buf, 1);
exit(0);
}else{
pid = getpid();
//同上原因
close(p1[0]);
close(p2[1]);
//向管道p1写入一个byte
write(p1[1],buf,1);
//如果读到了p2管道的byte,说明父进程收到了子进程发送的byte
if(read(p2[0],buf,1) == 1) {
printf("%d: received pong\n", pid);
}
exit(0);
}
}
}
- 在Makefile文件里面UPROGS 添加 $U/_pingpong\;
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
...
$U/_sleep\
$U/_pingpong\ //添加进Makefile
- 直接重新编译make qemu,或者用测试脚本测试,我这里就放一下用脚本测试的方法和结果;
jimmy@ubuntu:~/xv6-labs-2020$ ./grade-lab-util pingpong
make: 'kernel/kernel' is up to date.
== Test pingpong == pingpong: OK (1.4s)
primes函数实现
1.写一个素数筛选的程序,需要用到多进程和管道,这道题理解主要靠下面的图。
大概流程就是,输入2-11,然后发现2是素数,然后将能被2整除的素数全部筛掉。然后发现3是素数,在第二个阶段中将能被3整除的筛掉。
(牢记:每个进程的第一个数必定是素数)
2.根据上面的描述,需要写一个递归函数,这个函数要做的就是让父进程判断当前所有的数字中有没有能被第一个数整除的,如果不能整除,就用子进程把它送到下一次递归。然后利用这种链式结构,就像上面的图一样,完成筛选。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
void* child(int* p)
{
// 关闭上一个进程的写入端
close(p[1]);
int num;
//如果管道为空,说明已经筛选结束,直接关闭读取端,退出即可
if (read(p[0], &num, sizeof(num)) == 0)
{
close(p[0]);
exit(0);
}
//因为筛选规则,管道里面的第一个数一定是质数
printf("prime %d\n", num);
//创建一个管道用来写入这一轮被筛选的数据
int cp[2];
pipe(cp);
//将数据写入子进程中
if (fork() == 0) /
{
//一直递推,形成一种链式结构,让所有不能被当前质数整除的数据(在cp中)传递到下一个进程
child(cp);
}else { //处理从上一个进程收进来的管道数据
close(cp[0]);
int nnum;
//管道数据不会因为读而减少,因此子进程可以循环读,
//将所有不能被当前质数整除的数据传递到孙子进程的管道cp
while(read(p[0], &nnum, sizeof(nnum)))
// process 1 write an number, then process 2 read from the pipe
{
//不能被整除,就放到cp管道
if (nnum % num != 0)
{
write(cp[1], &nnum, sizeof(nnum));
}
}
//关闭cp的写入端
close(cp[1]);
wait(0);
}
exit(0);
}
int main(int argc, char const *argv[])
{
int p[2];
pipe(p);
//父进程初始化管道数据,然后用子进程来处理,并以此形成一种链式结构
if (fork() == 0) // child process
{
child(p);
}
else // parent process
{
close(p[0]);
for (int i = 2; i <= 35; i++)
{
write(p[1], &i, sizeof(i));
}
close(p[1]);
wait(0);
}
exit(0);
}
- 在Makefile文件里面UPROGS 添加 $U/_primes\;
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
...
$U/_sleep\
$U/_pingpong\
$U/_primes\ //添加进Makefile
- 直接重新编译make qemu,或者用测试脚本测试,我这里就放一下用脚本测试的方法和结果;
jimmy@ubuntu:~/xv6-labs-2020$ ./grade-lab-util primes
make: 'kernel/kernel' is up to date.
== Test primes == primes: OK (1.0s)
find函数
1.这个函数主要是用来找文件的路径,这个和ls命令有很多类似的地方,比如要读取目录树,确定当前是目录还是文件等等。因此,参考ls函数的源码来修改即可.
注意:要求不要对’.‘或者’…'进行递归,而且需要使用strcmp()函数来比较两个字符串。
2.首先写一个函数basename,这个函数用来返回一个完整路径下的文件名。(用于这个路径下的文件是不是target文件)
例如:“/home/user/documents/file.txt” -> file.txt
char *basename(char *pathname) {
char *prev = 0;
//分割出以第一个'/'为结尾的字符串
//cur+1就索引到了'/'之后的第一个字符
char *curr = strchr(pathname, '/');
//循环找,直到找到最后的文件名然后返回
while (curr != 0) {
prev = curr;
curr = strchr(curr + 1, '/');
}
return prev;
}
3.接下来写一个find函数,这个函数用于递归查找所有目录里,包含目标字符串的文件,并且打印出来,这个函数就是在ls函数的基础上修改的。
void find(char * curr_path, char * target)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
//当前路径下已经搜索完了或者文件不存在,就返回
if((fd = open(curr_path, 0)) < 0){
fprintf(2, "find: cannot open %s\n", curr_path);
return;
}
if(fstat(fd, &st) < 0){
fprintf(2, "find: cannot stat %s\n", curr_path);
close(fd);
return;
}
//判断当前类型
switch(st.type){
//如果是文件,说明搜索到底了
case T_FILE:;
char *file_name = basename(curr_path); //找到文件名
int match = 1;
//如果不是要找的文件名或者为空
if(file_name == 0 || strcmp(file_name+1, target) != 0){
match = 0;
}
//匹配成功,就把当前路径答应出来
if(match){
printf("%s\n",curr_path);
}
close(fd);
break;
//如果是一个目录,说明还要接着递归搜索
case T_DIR:
if(strlen(curr_path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
break;
}
strcpy(buf, curr_path);
p = buf+strlen(buf);
*p++ = '/';
while(read(fd, &de, sizeof(de)) == sizeof(de)){
if(de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}
if(strcmp(buf+strlen(buf)-2, "/.") != 0 && strcmp(buf+strlen(buf)-3, "/..") != 0) {
find(buf, target); // 递归查找
}
}
break;
}
close(fd);
}
}
4.最后是主函数,只需要做一个参数数目判断,然后调用find函数即可。
int main(int argc, char* argv[])
{
if (argc != 3) {
fprintf(2, "usage: find [directory] [target filename]\n");
exit(1);
}
find(argv[1], argv[2]);
exit(0);
}
5.在Makefile文件里面UPROGS 添加 $U/_find\;
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
...
$U/_sleep\
$U/_pingpong\
$U/_primes\
$U/_find\ //添加进Makefile
- 直接重新编译make qemu,或者用测试脚本测试,我这里就放一下用脚本测试的方法和结果;
jimmy@ubuntu:~/xv6-labs-2020$ ./grade-lab-util find
make: 'kernel/kernel' is up to date.
== Test find, in current directory == find, in current directory: OK (2.1s)
== Test find, recursive == find, recursive: OK (0.7s)
xargs函数
1.这个实验是写一个用来传参和执行命令的函数,主要有两个功能:一是可以实现对以’\n’为结尾的指令进行划分,然后执行,二是可以从管道获取其他命令(主要是指标准输入)传过来的参数。举个例子:
$ echo "1\n2" | xargs -n 1 echo line
xrags会将从管道将stdin输入"1\n2"拼接在echo line后:
$ echo line 1
$ \n
$ echo line 2
运行结果为:
$ line 1
$ line 2
2.因此首先定义一个能够执行每个指令的函数run,方便调用
//op就是传进来的指令,args是参数
void run(char *op, char **args){
if(fork() == 0){
exec(op,args);
exit(0);
}
return;
}
3.然后写一个能够从管道获取参数,并且拼接参数的主函数
nt main(int argc, char* argv[])
{
char buf[2048]; // 读入时使用的内存池
char *p = buf, *last_p = buf; //当前参数的结束、开始指针
// 全部参数列表,字符串指针数组,包含 argv 传进来的参数和 stdin 读入的参数
char *argsbuf[128];
char **args = argsbuf;
//将 argv 提供的参数加入到最终的参数列表中
//可以用数组的角度去看这是个指针,vector<string> *args = argsbuf[0],*args +1 ...
for(int i = 1; i < argc; i++)
{
*args = argv[i];
args++;
}
// 开始读入从管道传过来的参数,然后根据换行符把相应的参数拼接上去
char **pa = args;
while(read(0, p, 1) != 0) {
if(*p == ' ' || *p == '\n') {
// 读入一个参数完成(以空格分隔,如 `echo hello world`,则 hello 和 world 各为一个参数)
// 读入一行完成,执行指令
if(*p == '\n') {
*pa = 0; // 参数列表末尾用 null 标识列表结束
run(argv[1], argsbuf);
pa = args; // 重置读入参数指针,准备读入下一行
}
*p = '\0';
// 将空格替换为 \0 分割开各个参数,这样可以直接使用内存池中的字符串作为参数字符串
// 而不用额外开辟空间
*(pa++) = last_p;
last_p = p+1;
}
p++;
}
//如果最后一行不是空行,还有未处理的参数
if(pa != args) {
// 收尾最后一个参数
*p = '\0';
*(pa++) = last_p;
// 收尾最后一行
*pa = 0; // 参数列表末尾用 null 标识列表结束
// 执行最后一行指令
run(argv[1], argsbuf);
}
while(wait(0) != -1) {}; // 循环等待所有子进程完成,每一次 wait(0) 等待一个
exit(0);
}
4.在Makefile文件里面UPROGS 添加 $U/_xargs\;
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
...
$U/_sleep\
$U/_pingpong\
$U/_primes\
$U/_find\
$U/_xargs\ //添加进Makefile
- 直接重新编译make qemu,或者用测试脚本测试,我这里就放一下用脚本测试的方法和结果;
jimmy@ubuntu:~/xv6-labs-2020$ ./grade-lab-util xargs
make: 'kernel/kernel' is up to date.
== Test xargs == xargs: OK (2.0s)
总结
lab1还有几个补充实验,并没有做。
父子进程:子进程会完全copy父进程的页表,指向同一块物理内存区域。然后当父子进程修改各自内容时,发生段页错误,然后copy on write,进行物理内存的实际分配,二者是完全独立的运行空间。
本文主要用于自己学习记录,然后参考了几位大佬的blog,代码做了详细注释。
[1]:https://pdos.csail.mit.edu/6.S081/2020/labs/util.html
[2]: https://blog.miigon.net/posts/s081-lab1-unix-utilities/
[3]: https://zhuanlan.zhihu.com/p/624091268