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),具体多少多次时间一滴答我不太清楚。

​ 官网的原文:

c 复制代码
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目录下。

在编码前,我们先看看来自官网的提示:

  1. 在开始编码前,请阅读 xv6 书籍的第 1 章。
  2. 将你的代码写在 user/sleep.c 文件中。参考 user/ 目录下的其他程序(例如 user/echo.cuser/grep.cuser/rm.c),了解命令行参数是如何传递给程序的。
  3. 将你的 sleep 程序添加到 MakefileUPROGS 列表中;完成这一步后,执行 make qemu 会编译你的程序,且你能在 xv6 的 shell 中运行它。
  4. 如果用户忘记传递参数,sleep 程序应当打印一条错误信息。
  5. 命令行参数是以字符串形式传递的;你可以使用 atoi 函数将其转换为整数(参考 user/ulib.c)。
  6. 使用 pause() 系统调用来让进程暂停。
  7. 可参考以下文件理解 pause() 的实现:1 .kernel/sysproc.c:实现 pause() 系统调用的 xv6 内核代码(查找 sys_pause 函数);2 .user/user.h:用户程序中可调用的 pause() 函数的 C 语言声明;3 .user/usys.S:从用户代码跳转到内核执行 pause() 的汇编代码。
  8. 可参考 Kernighan 和 Ritchie 所著的《C 程序设计语言(第二版)》学习 C 语言。

​ 以下是user/sleep.c当中的代码实现,你可以通过上面的提示和接下来的代码块来理解一下。

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当中哦~

makefile 复制代码
// 大概在接近200行的位置,有类似于以下的内容,照葫芦画瓢将sleep.c写入。
UPROGS=\
	$U/_cat\
	$U/_echo\
	$U/_forktest\
	$U/_find\
	$U/_grep\
	...
	...
	$u/_sleep\  【像这样】

4.xv6系统调用的底层逻辑

​ 刚才我们动手实现了sleep系统调用并且可以在xv6的命令行当中通过sleep n的方式启动sleep程序,完成挂起进程的操作,为什么这个sleep可以在命令行中使用指令启动呢?

  1. 我们在xv6的shell当中输入sleep n后,shell读到的是一行字符串:"sleep n\n";输入完指令按下回车的那一刻,字符串"sleep n\n"(后面的\n是你刚才按下的回车)会送往 shell 进程的标准输入 ,之后sh这个程序会从标准输入当中读取刚才的字符串,然后开始调用相应的函数进行解析。在user/sh.c当中有三个函数,分别是getcmd()会从字符串当中读取一条命令、parsecmd()解析成符合xv6标准的结构和runcmd()执行指令。其中解析的结果大概是:

    c 复制代码
    type = EXEC  //其中EXEC代表这是指令
    argv[0] = "sleep"
    argv[1] = "n" //n是一个整数
    argv[2] = 0
  2. 此时在进入runcmd后,我们将字符串"sleep n"解析为了struct execcmd *ecmd类型的,然后执行调用fork()函数新建一个子进程然后通过exec(ecmd->argv[0], ecmd->argv)方法将子进程替换为sleep,然后开始执行sleep.c当中的内容(从main开始)。

  3. 在sleep.c当中main()会调用pause()系统调用来实现挂起功能。

    user/user.h声明用户态可调用的 sleep() 接口;

    kernel/syscall.h定义系统调用号 SYS_sleep

    kernel/sysproc.c实现内核态的 sys_sleep()函数;

    kernel/syscall.c:根据系统调用号完成从用户态到内核态的分发。

  4. 通过内核函数sleep,进入睡眠状态(可能是把进程挂入"睡眠队列"中)。过程依赖时钟中断,每一次时钟中断会递增全局的 tick 计数,当进程在内核中等待 tick 数达到指定值之前处于阻塞状态,当条件满足后,进程被唤醒,继续执行。(本人猜测:每次时钟中断都会递增全局的 ticks 计数,并调用 wakeup(&ticks) 唤醒所有等待该通道的进程。被唤醒的进程重新运行后,会在 sys_sleep 中检查当前 ticks 是否已达到指定值,若未满足则继续进入睡眠,直到条件满足后返回继续执行。)。

  5. 返回用户态,sleep程序exit结束。

  6. 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目录下。

​ 在编码前,我们先看看来自官网的提示:

  1. 逐个字符地读取输入文件。
  2. 你可以使用 strchr(参考 user/ulib.c)来测试某个字符是否属于分隔符。
  3. 文件的开头和结尾隐式地被视为分隔符。

​ 以下是user/sixfive.c当中的代码实现,你可以通过上面的提示和接下来的代码块来理解一下。

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 语言字符串的字节内容;打印该字符串。

​ 记得要参考官网给出的例子的输出格式。

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.

​ 在编码前记得先看看官网的提示:

  1. 查看 user/ls.c,学习如何读取目录内容。
  2. 使用递归,让 find 可以进入子目录查找。
  3. 不要递归进入 "."".." 目录。
  4. 每次调用 make(或相关命令)都会生成一个新的 fs.img,之前运行创建的文件会被删除。如果你想用上一次的文件系统,可以使用 make qemu-fs 启动 QEMU。
  5. 你需要使用 C 字符串(null 结尾的字符数组)。可以参考 K&R 书中第 5.5 节。
  6. 注意:== 并不像 Python 那样可以比较字符串内容,要使用 strcmp() 来比较两个 C 字符串。
  7. 将你的程序添加到 MakefileUPROGS 中。
c 复制代码
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.

​ 编码前要看来自官网的提示:

  • 使用 forkexec 来在每个匹配的文件上执行指定的命令。fork() 创建一个子进程。子进程用 exec() 替换为你要执行的命令(例如 echo hi ./file)。父进程使用 wait() 等待子进程完成命令执行。
  • kernel/param.h 中声明了 MAXARG,如果你需要定义一个 argv 数组来存放命令及其参数,这个常量会很有用。
c 复制代码
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来进行评分操作。

​ 以下是输出内容:

shell 复制代码
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,所以会更新很慢。

​ 有什么错误问题可以联系我,我也会持续维护这些内容。

相关推荐
HLC++11 小时前
Linux的进程间通信
android·linux·服务器
groundhappy14 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕15 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫16 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生16 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1118 小时前
linux基础IO
linux·运维·服务器
BelongPanda20 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯20 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983420 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei3720 小时前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源