Lab1-Xv6 and Unix utilities 配置环境的搭建以及前言 && MIT6.1810操作系统工程【持续更新】
Lab: Xv6 and Unix utilities(未完待续)
在这个,也是第一个Lab当中6.1810 / Fall 2025,它会要求你通过git拉取最基本的内核代码,然后cd到内核代码目录当中,通过指定的指令(下面会介绍)即可构建起xv6操作系统。
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;在
kernel/sysproc.c实现内核态的sys_sleep()函数;kernel/syscall.c:根据系统调用号完成从用户态到内核态的分发。 -
通过
内核函数sleep,进入睡眠状态(可能是把进程挂入”睡眠队列“中)。过程依赖时钟中断,每一次时钟中断会递增全局的 tick 计数,当进程在内核中等待 tick 数达到指定值之前处于阻塞状态,当条件满足后,进程被唤醒,继续执行。(本人猜测:每次时钟中断都会递增全局的 ticks 计数,并调用 wakeup(&ticks) 唤醒所有等待该通道的进程。被唤醒的进程重新运行后,会在 sys_sleep 中检查当前 ticks 是否已达到指定值,若未满足则继续进入睡眠,直到条件满足后返回继续执行。)。 -
返回用户态,sleep程序exit结束。
-
shell wait返回,shell等待下一条命令。
5.sixfive【中等】
这一部分让我们使用系统调用read,open来打开一个文件,并且打印文件当中所有是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 shouldn't 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);
data+=1;
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调用,在实现该功能的时候会用到open,read,fstat等系统调用。
官网的原文:
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 结尾的字符数组)。可以参考 K&R 书中第 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".)。
会用到fork,exec,wait等系统调用。
官网原文:
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
xiaobai@LAPTOP-JEJ8JHE6:~/xv6-labs-2025$
可以看到拿到了130分,差的一分应该是time.txt,这个我们没有在官网当中找到相应的内容,所以也不再死扣这一分了。
10.写在后面
接下来要开始研究6.1810 / Fall 2025了。由于还要复习408,所以会更新很慢。
有什么错误问题可以联系我,我也会持续维护这些内容。

浙公网安备 33010602011771号