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,所以會更新很慢。
有什麼錯誤問題可以聯繫我,我也會持續維護這些內容。