(undone) MIT6.S081 2023 一个月速通 (Day2: LAB1 Utilities)
实验网页:https://pdos.csail.mit.edu/6.S081/2023/labs/util.html
从网页描述中可以得到一些有用的信息:
1.可以执行 make grade
来测试自己的代码,获取得分。但这个命令会同时运行所有测试。
2.如果要单独对某个作业进行测试,有两种命令 (在 host 上执行,不是在 xv6 里执行)
./grade-lab-util sleep
make GRADEFLAGS=sleep grade
这次 LAB 一共有五个实验,如下:
- sleep 简单
- pingpong 简单
- prime 困难
- find 中等
- xargs 中等
我们开始吧
任务1:完成 sleep 作业 (完成)
根据 LAB 网页要求,添加一个 sleep 命令,实现 sleep 10
能暂停 10 秒即可
这部分涉及到的源码其实很多,比如:
1.sleep 如何被编译进 fs.img
2.timer是怎么计数的
3.内核怎么切换进用户态,把 sleep 加载从磁盘加载进内存等等
但如果要把这些代码都看懂再开始做,那就太久了,这也不是我们平时做科研和工程的方式。
坚持使用最小的代价完成任务,等所有 LABS 做完,再去查漏补缺,或许这才是做 xv6 实验的正确方式。
但我们最好尽量不看讲义,毕竟现实的科研和工程没有讲义,只有一个 TARGET
现在的目标很明确,就是添加一个 sleep 命令。
首先,我们有和 sleep 命令相同的命令,比如 ls 命令,执行效果如下:
由于 sleep 命令和 ls 命令都是用户程序,那么参考 ls 命令的实现方式一定对我们实现 sleep 命令大有帮助,我们先看 ls.c 文件
可以在 user/ls.c 看到 ls 命令实现细节,如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"char*
fmtname(char *path)
{static char buf[DIRSIZ+1];char *p;// Find first character after last slash.for(p=path+strlen(path); p >= path && *p != '/'; p--);p++;// Return blank-padded name.if(strlen(p) >= DIRSIZ)return p;memmove(buf, p, strlen(p));memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));return buf;
}void
ls(char *path)
{char buf[512], *p;int fd;struct dirent de;struct stat st;if((fd = open(path, O_RDONLY)) < 0){fprintf(2, "ls: cannot open %s\n", path);return;}if(fstat(fd, &st) < 0){fprintf(2, "ls: cannot stat %s\n", path);close(fd);return;}switch(st.type){case T_DEVICE:case T_FILE:printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);break;case T_DIR:if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){printf("ls: path too long\n");break;}strcpy(buf, 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("ls: cannot stat %s\n", buf);continue;}printf("%s %d %d %d\n", fmtname(buf), st.type, st.ino, st.size);}break;}close(fd);
}int
main(int argc, char *argv[])
{int i;if(argc < 2){ls(".");exit(0);}for(i=1; i<argc; i++)ls(argv[i]);exit(0);
}
可以看到 ls 命令的实现中使用了类似 open
,exit
,close
这样的系统调用 API,如果我们代码跟踪进去,应该就能看到 xv6 目前所有支持的系统调用 API
从 open
跟踪进去,可以看到在 user/user.h 中定义的一堆 API
struct stat;// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(const char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
void *memmove(void*, const void*, int);
char* strchr(const char*, char c);
int strcmp(const char*, const char*);
void fprintf(int, const char*, ...);
void printf(const char*, ...);
char* gets(char*, int max);
uint strlen(const char*);
void* memset(void*, int, uint);
void* malloc(uint);
void free(void*);
int atoi(const char*);
int memcmp(const void *, const void *, uint);
void *memcpy(void *, const void *, uint);
可以看到,这是 xv6 提供给用户程序的系统调用,以及一些用户态库函数
在 23 行惊讶地发现,xv6 已经提供了 sleep() 系统调用 API,那我估计这事儿应该很容易完成
直接在 user/ 文件夹下添加一个 sleep.c 文件,代码如下:
#include "kernel/types.h" // user.h 自定义了数据类型
#include "user/user.h"int
main(int argc, char *argv[])
{if(argc != 2){fprintf(2, "usage: sleep [seconds]\n");exit(1);}sleep(atoi(argv[1]));exit(0);}
由于这是一个新的 C 文件,并没有在 Makefile 里添加依赖,那么很自然的想要在 Makefile 里做对应的修改。首先在 Makefile 里搜索 ls,在 Makefile : 174 找到对应的 UPROGS 用户程序列表,我们把 sleep 添加进去,如下:
UPROGS=\$U/_cat\$U/_echo\$U/_forktest\$U/_grep\$U/_init\$U/_kill\$U/_ln\$U/_ls\$U/_mkdir\$U/_rm\$U/_sh\$U/_stressfs\$U/_usertests\$U/_grind\$U/_wc\$U/_zombie\$U/_sleep\
运行 make qemu,运行 sleep 10 看看效果
sleep 10 确实停留了一小会儿,但不到两秒就结束了。观察 MIT6.S081 讲义,也没有说停留多久,只是说停留一小会儿
我们跑跑测试看看
测试通过,这个真是出乎意料的简单。
让我们提升一下难度,研究为什么这个 sleep 10 仅仅持续了不到2秒就结束 -------- start
这部分涉及的代码和知识点也有点多,反正 xv6 实验讲义也没要求,先 skip 吧。过完实验再回来思考
让我们提升一下难度,研究为什么这个 sleep 10 仅仅持续了不到2秒就结束 -------- end
任务2:完成 pingpong 作业(完成)
这个作业很简单,就是使用 fork, pipe, getpid, write, read 等系统调用实现一个 “两个进程通过导管来回传输字符” 的例子。
代码如下:
#include "kernel/types.h" // user.h 自定义了数据类型
#include "user/user.h"int main() {int ftoc[2]; // 管道1:父进程到子进程int ctof[2]; // 管道2:子进程到父进程// 0 是读端,1是写端// 创建管道if (pipe(ftoc) == -1 || pipe(ctof) == -1) {fprintf(2, "error: create pipe\n");exit(1);}int pid = fork();if (pid < 0) {fprintf(2, "error: fork\n");exit(1);}if (pid == 0) { // 子进程// 子进程 pidint child_pid = getpid();// 从父进程读取一个字节char receivemsg = '\0';read(ftoc[0], &receivemsg, 1);printf("%d: received ping\n", child_pid);printf("%d: received %c\n", child_pid, receivemsg);// 向父进程发送一个字节char sendmsg = 'B'; // 发送的字节write(ctof[1], &sendmsg, 1);close(ftoc[0]); // 关闭父到子管道的读端close(ftoc[1]); // 关闭父到子管道的写端close(ctof[0]); // 关闭子到父管道的读端close(ctof[1]); // 关闭子到父管道的写端exit(0);} else { // 父进程// 父进程 pidint father_pid = getpid();// 向子进程发送一个字节char sendmsg = 'A'; // 发送的字节write(ftoc[1], &sendmsg, 1);// 从子进程读取一个字节char receivemsg = '\0';read(ctof[0], &receivemsg, 1);printf("%d: received pong\n", father_pid);printf("%d: received %c\n", father_pid, receivemsg);close(ftoc[0]); // 关闭父到子管道的读端close(ftoc[1]); // 关闭父到子管道的写端close(ctof[0]); // 关闭子到父管道的读端close(ctof[1]); // 关闭子到父管道的写端exit(0);}exit(0);
}
分别是 user/pingpong.c,以及在 Makefile 相应位置加上 pingpong
先运行 make qemu
进行编译,随后执行测试,通过
任务3:完成 prime 作业
TODO: here
任务4:完成 find 作业
TODO: here
任务5:完成 xargs 作业
TODO: here