博客 / 詳情

返回

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目錄下。

在編碼前,我們先看看來自官網的提示:

  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當中的代碼實現,你可以通過上面的提示和接下來的代碼塊來理解一下。

// 參考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可以在命令行中使用指令啓動呢?

  1. 我們在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
    
  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當中的代碼實現,你可以通過上面的提示和接下來的代碼塊來理解一下。

#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.

​ 在編碼前記得先看看官網的提示:

  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 中。
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 數組來存放命令及其參數,這個常量會很有用。
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,所以會更新很慢。

​ 有什麼錯誤問題可以聯繫我,我也會持續維護這些內容。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.