Lab: Xv6 and Unix utilities

发布时间:2026/6/21 8:29:32

Lab: Xv6 and Unix utilities 1.拉取基本代码​注意由于之前Lab0 配置环境的搭建以及前言 MIT6.1810操作系统工程 的文章中提过本人的环境Win11当中的ubuntu子系统因此本人在这里就不再过多强调了。​ 我们通过官网给出的的指令git clone git://g.csail.mit.edu/xv6-labs-2025来拉取代码到本地目录过程会耗费一些时间github的服务器在海外所以会下载的很慢慢慢等就好了。​ 在拉取完成后我们通过指令cd xv6-labs-2025来切换到我们刚才拉取的目录。2.构建并且运行xv6​构建xv6所用的qemu的版本需要≥7.2.0因此我们可以通过在系统终端指令qemu-system-riscv64 --version来确认我们的qemu版本如果qemu不是7.2.0则需要更新又因为可能会存在这样的情况官方源最高支持到qemu 6.x.x所以这边建议下载≥ qemu 7.2.0版本的源代码然后自己编译它安装具体问AI。​ 假设你已经安装好了确保我们在xv6-labs-2025目录当中之后在命令行输入make qemu指令来构建xv6当我们能看到xv6 kernel is booting hart 2 starting hart 1 starting init: starting sh $​ 出现这些字样后代表编译成功如果编译出错多半是因为qemu版本的问题上面有解决方法小概率是文件权限的问题你可能在拉取代码时使用了sudo权限的问题可以先尝试sudo make qemu如果不行再问AI。​ 现在内核已经被启动接下来你可以通过Ctrl a 再按x退出xv6的终端返回ubuntu的终端。然后输入指令“code ./来启动vscode启动完成后vscode的页面左侧的文件就是xv6的内核文件其中kernel 目录 下是内核态源码user下是用户态源码。3.实现sleep用户程序【简单】​ 这一部分要求我们实现一个运行在用户态的程序sleep这个sleep会调用pause()这个系统调用来挂起进程成品的效果是输入sleep n后xv6内核会挂起当前的用户进程n个时钟滴答数ticks具体多少多次时间一滴答我不太清楚。​ 官网的原文Implement a user-level sleep program for xv6, along the lines of the UNIX sleep command. Your sleep should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the file user/sleep.c.​ 要求我们把sleep的程序写入user/sleep.c当中我们可以在user目录下寻找sleep.c文件如果没有的话就自己创建一个sleep.c文件在user目录下。在编码前我们先看看来自官网的提示在开始编码前请阅读 xv6 书籍的第 1 章。将你的代码写在user/sleep.c文件中。参考user/目录下的其他程序例如user/echo.c、user/grep.c和user/rm.c了解命令行参数是如何传递给程序的。将你的sleep程序添加到Makefile的UPROGS列表中完成这一步后执行make qemu会编译你的程序且你能在 xv6 的 shell 中运行它。如果用户忘记传递参数sleep程序应当打印一条错误信息。命令行参数是以字符串形式传递的你可以使用atoi函数将其转换为整数参考user/ulib.c。使用pause()系统调用来让进程暂停。可参考以下文件理解pause()的实现1.kernel/sysproc.c实现pause()系统调用的 xv6 内核代码查找sys_pause函数2.user/user.h用户程序中可调用的pause()函数的 C 语言声明3.user/usys.S从用户代码跳转到内核执行pause()的汇编代码。可参考 Kernighan 和 Ritchie 所著的《C 程序设计语言第二版》学习 C 语言。​​ 以下是user/sleep.c当中的代码实现你可以通过上面的提示和接下来的代码块来理解一下。// 参考user/echo.c当中的头文件调用 #include kernel/types.h #include user/user.h int main(int argc, char *argv[]) { if(argc 2){ // 没有传参则打印一条错误信息 printf(Usage: sleep seconds\n); exit(1); } //参考官网当中的提示我们要调用pause并且使用atoi来做类型转换 pause(atoi(argv[1])); exit(0); }注意不要忘记在编译程序前将sixfive添加到MakeFile当中的UPROGS当中哦~// 大概在接近200行的位置有类似于以下的内容照葫芦画瓢将sleep.c写入。 UPROGS\ $U/_cat\ $U/_echo\ $U/_forktest\ $U/_find\ $U/_grep\ ... ... $u/_sleep\ 【像这样】4.xv6系统调用的底层逻辑未完之后会再次修改​ 刚才我们动手实现了sleep系统调用并且可以在xv6的命令行当中通过sleep n的方式启动sleep程序完成挂起进程的操作为什么这个sleep可以在命令行中使用指令启动呢​我们在xv6的shell当中输入sleep n后shell读到的是一行字符串“sleep n\n”输入完指令按下回车的那一刻字符串“sleep n\n”后面的\n是你刚才按下的回车会送往shell 进程的标准输入之后sh这个程序会从标准输入当中读取刚才的字符串然后开始调用相应的函数进行解析。在user/sh.c当中有三个函数分别是getcmd()会从字符串当中读取一条命令、parsecmd()解析成符合xv6标准的结构和runcmd()执行指令。其中解析的结果大概是type EXEC //其中EXEC代表这是指令 argv[0] sleep argv[1] n //n是一个整数 argv[2] 0此时在进入runcmd后我们将字符串“sleep n”解析为了struct execcmd *ecmd类型的然后执行调用fork()函数新建一个子进程然后通过exec(ecmd-argv[0], ecmd-argv)方法将子进程替换为sleep然后开始执行sleep.c当中的内容从main开始。在sleep.c当中main()会调用pause()系统调用来实现挂起功能。在user/user.h声明用户态可调用的sleep()接口在kernel/syscall.h定义系统调用号SYS_sleep方便syscall使用在kernel/sysproc.c实现内核态的sys_sleep()函数最终会调用该函数实现我们想要的功能kernel/syscall.c根据系统调用号完成从内核态调用函数的分发1、取出进程想要调用的系统调用号2、通过调用号在调用函数指针表当中寻找对应的系统调用函数的指针3、存入进程的某个寄存器当中4、进程开始通过该指针进行调用。通过内核函数sleep进入睡眠状态可能是把进程挂入”睡眠队列“中。过程依赖时钟中断每一次时钟中断会递增全局的 tick 计数当进程在内核中等待 tick 数达到指定值之前处于阻塞状态当条件满足后进程被唤醒继续执行。本人猜测每次时钟中断都会递增全局的 ticks 计数并调用 wakeup(ticks) 唤醒所有等待该通道的进程。被唤醒的进程重新运行后会在 sys_sleep 中检查当前 ticks 是否已达到指定值若未满足则继续进入睡眠直到条件满足后返回继续执行。。返回用户态sleep程序exit结束。shell wait返回shell等待下一条命令。5.sixfive【中等】​ 这一部分让我们使用系统调用readopen来打开一个文件并且打印文件当中所有是5和6的倍数的数字。​ 官网的原文For each input file, sixfive must print all the numbers in the file that are multiples of 5 or 6. Number are a sequence of decimal digits separated by characters in the string -\r\t\n./,. Thus, for the six in xv6 sixfive shouldnt print 6 but, for example, /6, it should.​ 要求我们把sixfive的程序写入user/sixfive.c当中我们可以在user目录下寻找sixfive.c文件如果没有的话就自己创建一个sixfive.c文件在user目录下。​ 在编码前我们先看看来自官网的提示逐个字符地读取输入文件。你可以使用strchr参考user/ulib.c来测试某个字符是否属于分隔符。文件的开头和结尾隐式地被视为分隔符。​ 以下是user/sixfive.c当中的代码实现你可以通过上面的提示和接下来的代码块来理解一下。#include kernel/types.h #include kernel/fcntl.h //定义了打开文件的方式 #include user/user.h int main(int argc, char *argv[]) { if(argc 2){ printf(Usage: sixfive file1 [file2 ...]\n); exit(1); } // 官网当中说了可能会传入多个文件这里我们使用循环依次接收所有文件 for(int i 1; i argc; i){ int fd open(argv[i], O_RDONLY); if(fd 0){ printf(open %s failed\n, argv[i]); continue; // 继续处理下一个文件 } char c; int num 0; int in_numbers 0; // 逐个字符读取 while(read(fd, c, 1) 1){ if(c 0 c 9){ num num * 10 (c - 0); in_numbers 1; } else { if(in_numbers (num % 5 0 || num % 6 0)){ printf(%d\n, num); } num 0; in_numbers 0; } } // 文件结尾也要处理最后一个数字 if(in_numbers (num % 5 0 || num % 6 0)){ printf(%d\n, num); } close(fd); } exit(0); }​ 记得写完程序后要保证你程序的输出要和官网的例示一致这样在之后的makr grade当中才能通过得分检测。​注意不要忘记在编译程序前将sixfive添加到MakeFile当中的UPROGS当中哦~6.memdump【简单】​ 这一部分用到了不少的类型转换和指针相关内容不会的话可以先去看相关教程和教材又或者 “GPT/豆包/deepseek 启动”问AI。它似乎提前准备好了user/memdump.c这个文件进入里面你自然会看到一个等你实现的函数。说白了memdump函数有两个参数fmt是格式data是数据。我们要做的是将输入的数据按照fmt指定的格式打印出来。​ 在编码前我先看一下来着官网的格式要求注意区分大小写i将数据的接下来的 4 个字节作为一个 32 位整数以十进制打印。p将数据的接下来的 8 个字节作为一个 64 位整数以十六进制打印。h将数据的接下来的 2 个字节作为一个 16 位整数以十进制打印。c将数据的接下来的 1 个字节作为一个 8 位 ASCII 字符打印。s数据的接下来的 8 个字节是一个指向 C 语言字符串的 64 位指针打印该字符串。S数据的剩余部分包含一个以空字符结尾的 C 语言字符串的字节内容打印该字符串。​ 记得要参考官网给出的例子的输出格式。void memdump(char *fmt, char *data) { // Your code here. // 读取格式 char *log_fmt fmt; // 据我观察有多少格式字符就对应有多少数据所以我们以格式字符串的长度作为参考进行循环 while(*log_fmt ! \0){ switch(*log_fmt){ case i: { //i将数据的后续 4 字节内容以十进制形式打印为一个 32 位整数。 uint32 int32_num *(uint32 *)data; printf(%d\n,int32_num); data 4; break; } case p: { //p将数据的后续 8 字节内容以十六进制形式打印为一个 64 位整数。 uint64 int64_num *(uint64 *)data; printf(%lx\n,int64_num); data 8; break; } case h: { //h将数据的后续 2 字节内容以十进制形式打印为一个 16 位整数。 uint16 int16_num *(uint16 *)data; printf(%d\n,int16_num); data 2; break; } case c: { //c将数据的后续 1 字节内容以 8 位 ASCII 字符形式打印。 char ch *(char *)data; printf(%c\n,ch); data1; break; } case s: { //s数据的后续 8 字节内容为一个指向 C 语言字符串的 64 位指针打印该字符串。 char *str *(char **)data; printf(%s\n, str); data 8; break; } case S: { //S数据的剩余部分为一个以空字符结尾的 C 语言字符串的字节内容打印该字符串。 printf(%s\n,data); break; } default: break; } //自增格式串指针 log_fmt; } }7.find【中等】​ 这一部分的练习是实现一个类似于Linux/Unix当中的find调用在实现该功能的时候会用到openreadfstat等系统调用。​ 官网的原文Write a simple version of the UNIX find program for xv6: find all the files in a directory tree with a specific name. Your solution should be in the file user/find.c.​ 在编码前记得先看看官网的提示查看user/ls.c学习如何读取目录内容。使用递归让find可以进入子目录查找。不要递归进入.和..目录。每次调用make或相关命令都会生成一个新的fs.img之前运行创建的文件会被删除。如果你想用上一次的文件系统可以使用make qemu-fs启动 QEMU。你需要使用 C 字符串null 结尾的字符数组。可以参考 KR 书中第 5.5 节。注意并不像 Python 那样可以比较字符串内容要使用strcmp()来比较两个 C 字符串。将你的程序添加到Makefile的UPROGS中。void find(char *path,char *filename) { 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; } // 如果是普通文件判断名字是否匹配 if(st.type T_FILE){ // 取 path 中最后一个 / 后的文件名 char *name path strlen(path); while(name path *name ! /) name--; name; if(strcmp(name, filename) 0){ printf(%s\n, path); } close(fd); return; } // 如果是目录递归遍历 if(st.type T_DIR){ if(strlen(path) 1 DIRSIZ 1 sizeof buf){ fprintf(2, find: path too long\n); close(fd); return; } strcpy(buf, path); p buf strlen(buf); *p /; while(read(fd, de, sizeof(de)) sizeof(de)){ if(de.inum 0){ continue; } // 跳过 . 和 ..提示要求 if(strcmp(de.name, .) 0 || strcmp(de.name, ..) 0){ continue; } memmove(p, de.name, DIRSIZ); p[DIRSIZ] 0; // 递归调用 find(buf, filename); } } close(fd); } int main(int argc, char *argv[]) { if(argc 3){ exit(0); } find(argv[1], argv[2]); exit(0); }8.exec【中等】​ 这一部分我们需要对上面的find函数进行一些修改大致的要求是我们输入find . wc -exec echo hi后调用之前的find然后我们在要输出最终结果的时候对find进行修改将原本的输出“./wc” 变为“hi ./wc”。官网要求The following example illustrates find -exec behavior: Note that the command here is echo hi and the file is ./wc, making the command echo hi ./wc, which outputs hi ./wc.。​ 会用到forkexecwait等系统调用。​ 官网原文Add a -exec cmd to find, which executes the program cmd file for each file f that find finds, instead of printing matching file names.​ 编码前要看来自官网的提示使用fork和exec来在每个匹配的文件上执行指定的命令。fork()创建一个子进程。子进程用exec()替换为你要执行的命令例如echo hi ./file。父进程使用wait()等待子进程完成命令执行。kernel/param.h中声明了MAXARG如果你需要定义一个argv数组来存放命令及其参数这个常量会很有用。void find(char *path,char *filename,char* tip_comm,char *command,char *parameter) { 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; } // 如果是普通文件判断名字是否匹配 if(st.type T_FILE){ // 取 path 中最后一个 / 后的文件名 char *name path strlen(path); while(name path *name ! /) name--; name; if(strcmp(name, filename) 0){ //在这里作修改 if(tip_comm NULL){ printf(%s\n, path); return; } int pid fork(); if(pid 0 ){ // 父进程等待 wait(0); } else if(pid 0){ if(parameter ! NULL){ // 子进程执行 char *argv_s[] { command, parameter, path, 0 }; exec(command, argv_s); } else { char *argv_s[] { command, path, 0 }; exec(command, argv_s); } exit(1); } } close(fd); return; } // 如果是目录递归遍历 if(st.type T_DIR){ if(strlen(path) 1 DIRSIZ 1 sizeof buf){ fprintf(2, find: path too long\n); close(fd); return; } strcpy(buf, path); p buf strlen(buf); *p /; while(read(fd, de, sizeof(de)) sizeof(de)){ if(de.inum 0){ continue; } // 跳过 . 和 .. if(strcmp(de.name, .) 0 || strcmp(de.name, ..) 0){ continue; } memmove(p, de.name, DIRSIZ); p[DIRSIZ] 0; // 递归调用 find(buf, filename, tip_comm, command, parameter); } } close(fd); } int main(int argc, char *argv[]) { if(argc 6 strcmp(argv[3], -exec) 0){ find(argv[1], argv[2],argv[3], argv[4], argv[5]); exit(0); } find(argv[1], argv[2],NULL,NULL,NULL); exit(0); }注意要保证你的输出和官网当中给出的一致。9.收尾之 make grade​ 在完成了上面的所有内容后我们返回ubuntu的命令行保证当前目录在~/xv6-labs-2025输入指令make grad来进行评分操作。​ 以下是输出内容make[1]: Leaving directory /home/xiaobai/xv6-labs-2025 Test sleep, no arguments $ make qemu-gdb sleep, no arguments: OK (2.2s) Test sleep, returns $ make qemu-gdb sleep, returns: OK (0.4s) Test sleep, makes syscall $ make qemu-gdb sleep, makes syscall: OK (1.1s) Test sixfive_test $ make qemu-gdb sixfive_test: OK (1.0s) Test sixfive_readme $ make qemu-gdb sixfive_readme: OK (1.4s) Test sixfive_all $ make qemu-gdb sixfive_all: OK (1.0s) Test memdump, examples $ make qemu-gdb memdump, examples: OK (0.6s) Test memdump, format ii, S, p $ make qemu-gdb memdump, format ii, S, p: OK (1.0s) Test find, in current directory $ make qemu-gdb find, in current directory: OK (0.9s) Test find, in sub-directory $ make qemu-gdb find, in sub-directory: OK (1.1s) Test find, recursive $ make qemu-gdb find, recursive: OK (1.1s) Test exec $ make qemu-gdb exec: OK (0.9s) Test exec, multiple args $ make qemu-gdb exec, multiple args: OK (1.0s) Test exec, recursive find $ make qemu-gdb exec, recursive find: OK (1.2s) Test time time: FAIL Cannot read time.txt Score: 130/131 make: *** [Makefile:364: grade] Error 1 xiaobaiLAPTOP-JEJ8JHE6:~/xv6-labs-2025$​ 可以看到拿到了130分差的一分应该是time.txt这个我们没有在官网当中找到

相关新闻