lab-1
注:實驗環境在我的匯編隨手記的末尾部分有搭建教程。
0.前置
第零章
xv6為我們提供了多種系統調用,其中,exec將從某個文件里讀取內存鏡像(這確實是一個好的說法),并且將其替換到調用它的內存空間,也就是這個打開的文件(一切皆文件)替換了當前的進程,exec僅僅是這樣的功能,同時,執行完成之后,exec并不會返回當前的調用進程,而是執行我們已經加載好的指令!
如果你閱讀過手寫docker或者類似講過相關概念的書,你一定會知道,我們執行命令事實上是通過創建一個子進程,再在子進程中exec我們需要的命令!
exec并不是執行程序這個操作的全部,而只是將當前進程替換為某個可執行文件的工具,它需要結合 fork 使用,才是完整的執行命令的流程。
而命令執行完成,我們的子進程就會調用exit,使得我們的父進程從wait中返回。
**文件描述符是啥?**一切皆文件,我們的文件描述符可以是管道,文件,目錄,socket的抽象,但是,值得注意的是,文件描述符并不代表了這個文件,而是指向這個文件的"指針",使得我們可以對其進行訪問,我們可以獲取多個指向同一個文件的文件描述符,并且能夠對其進行寫入,讀取操作。
應用比如cat指令,cat并不關心你的文件描述符指向的是什么,使得我們可以輕松的實現cat指令,所以文件描述符是一個很棒的抽象。
甚至,fork和文件描述符可以實現我們的重定向,比如當我們fork一個進程之后,關閉子進程的文件描述符0(標準輸入),然后重新打開一個我們指定的文件,文件描述符0指向的是我們指定的文件,也就是說,我們的標準輸入來自于文件,而不是鍵盤了!然后我們執行cat,就會打印出我們的文件內容,指令為:cat < test.txt
。
一般來說,通過dup和fork產生的文件描述符都將共享同一個偏移量,但是有一些特殊情況,這里不詳細說了。
管道
這段代碼值得分析,先創建一個管道,讀端文件描述符為p[0],寫端為p[1],在我們的子進程中,先將文件描述符0(標準輸入)關閉,然后調用我們的dup將文件描述符p[0]復制到標準輸入中,此時,我們的wc就可以從文件中讀取數據了,然后,我們還需要子進程的寫端,因為子進程中,寫端是無用的,如果不關閉,我們在wc進程中的read將會阻塞,無法返回。
而在父進程中,指向寫入,然后關閉就行了。
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if(fork() == 0) {close(0);dup(p[0]);close(p[0]);close(p[1]);exec("/bin/wc", argv);
} else {write(p[1], "hello world\n", 12);close(p[0]);close(p[1]);
}
管道比臨時文件強大得多,管道支持自動銷毀,支持發送任意長度的數據,支持同步地進程間通信。
文件系統
我看這部分主要講的是文件就是一棵樹,前面沒啥好說的
mknod
表示創建一個設備文件,其元信息標志他是一個設備,并且記錄了主設備號和輔設備號,他們確定了唯一設備,當進程打開這個文件的時候,內核會將讀寫操作轉發到相應的設備上,而不是文件系統。
fstat
可以通過文件描述符獲取他所指向的文件的信息。
這里的一個概念也挺有意思的,就是文件名和文件有很大的區別,一個文件可以有多個文件名,一個文件名同一時刻指向一個文件(inode),比如說下面:
open("a", O_CREATE|O_WRONGLY);
link("a", "b");
這里創建了一個文件,然后通過link使得這個文件既叫a,又叫b,但是,此時我們如果執行unlink('a')
,我們的inode和磁盤空間并不會被清空,因為此時我們的文件名b還指向它,所以一個文件的的inode和磁盤空間只有link數量為0的時候才會被清除
所以
fd = open("/tmp/xyz", O_CREATE|O_RDWR);
unlink("/tmp/xyz");
是創建一個臨時inode的最好方式。
1. Sleep
挺簡單的,應該就是讓我們提升自信心的,先fork一個子進程,在子進程中調用sleep,父進程等待。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"int main(int argc, char *argv[]) {if (argc != 2) {fprintf(1, "Usage: sleep seconds\n");exit(1);} int pid = fork();if (pid == 0) {unsigned int seconds = atoi(argv[1]);sleep(seconds * 10);exit(0);} else {wait(0);}exit(0);
}
2. PingPong
大部分都是前置內容,也就是教材里面講過的,需要創建兩個管道,以供來相互通信,注意關閉讀寫端的時機:
#include "kernel/types.h"
#include "user/user.h"int main(int argc, char *argv[]) {int p1[2];int p2[2];pipe(p1);pipe(p2);int pid1 = fork();if (pid1 == 0) {close(p1[1]);close(p2[0]);char buf[1];read(p1[0], buf, 1);close(p1[0]);printf("%d: received ping\n", getpid());write(p2[1], "x", 1);close(p2[1]);exit(0);}int pid2 = fork();if (pid2 == 0) {close(p1[0]);close(p2[1]);write(p1[1], "x", 1);close(p1[1]);char buf[1];read(p2[0], buf, 1);close(p2[0]);printf("%d: received pong\n", getpid());write(p1[1], "x", 1);close(p1[1]);exit(0);}
}
3. Primes
我去,這個lab真牛逼,最核心的點就是dup去復用我們的管道,讓管道可以及時地被釋放,這真的很重要!否則你的程序大概率只能跑到40左右的數字(血的教訓),另外就是實驗要求使用埃拉托色尼篩法,這一點我最開始也搞不懂要怎么去在管道之間傳遞這個數字,其實就是pipe不熟悉,還是問了gpt才明白,可以一個一個傳,然后一個一個讀取。
然后dup的使用也是參考了別人的blog,感覺自己就是菜。
總之感覺還是挺神奇的。
#include "kernel/types.h"
#include "user/user.h"void primes(int p0[2]) __attribute__((noreturn));int main(int argc, char *argv[]) {int p[2];pipe(p);int pid = fork();if (pid == 0) {//管道的關閉邏輯在primes函數中primes(p);} else {close(p[0]);for (int i = 2; i <= 280; i++) {write(p[1], &i, sizeof(i));}close(p[1]);wait(0);}exit(0);
}void primes(int old_pipe[2]) {//及時釋放管道close(0);dup(old_pipe[0]);close(old_pipe[0]);close(old_pipe[1]);int prime;if (read(0, &prime, sizeof(prime)) == 0) {close(0);exit(0);}printf("prime %d\n", prime);//新建管道,并fork子進程int new_pipe[2];pipe(new_pipe);int pid = fork();if (pid == 0) {primes(new_pipe);} else {close(new_pipe[0]);int num;while (read(0, &num, sizeof(num))) {if (num % prime != 0) {write(new_pipe[1], &num, sizeof(num));}}close(0);close(new_pipe[1]);wait(0);}exit(0);
}
4. Find
實驗hint,讓我們可以從ls.c中知道怎么才可以展開當前目錄,這部分完全是參考了ls.c里面的方法,知道了這一點,我們就很好做判斷了。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"void find(char *path, char *filename);int main(int argc, char *argv[]) {if (argc != 3) {fprintf(2, "Usage: find filename with path\n");exit(1);}//遞歸搜索find(argv[1], argv[2]);exit(0);}void find(char *path, char *filename) {char buf[512], *p;int fd;struct dirent de;struct stat st;if ((fd = open(path, 0)) < 0) {fprintf(2, "find: cannot open %s\n", path);return;}if (fstat(fd, &st) < 0) {fprintf(2, "find: cannot stat %s\n", path);close(fd);return;}switch (st.type) {case T_DIR:if (strlen(path) + 1 + DIRSIZ + 1 >= sizeof(buf)) {printf("find: path too long\n");break;}strcpy(buf, path);p = buf + strlen(buf);*p++ = '/';while (read(fd, &de, sizeof(de)) == sizeof(de)) {if (de.inum == 0)continue;memmove(p, de.name, DIRSIZ);p[DIRSIZ] = 0;if (stat(buf, &st) < 0) {printf("find: cannot stat %s\n", buf);continue;}if (st.type == T_FILE && strcmp(de.name, filename) == 0) {printf("%s\n", buf);}if (st.type == T_DIR && strcmp(de.name, ".") != 0 && strcmp(de.name, "..") != 0) {find(buf, filename);}}break;default:if (strcmp(path, filename) == 0) {printf("%s\n", path);}}close(fd);
}
這里有一個很有意思很有意思的東西,我直接跳轉到read的實現,實際上但是他會直接跳到qemu的文件里面,導致我以為我們的read是qemu封裝好的,但是實際上并不是,read確確實實我們的xv6自己實現的!我們可以通過這樣去追溯它的根源:
- 在/user/usys.S中,找到有關read的字段,可以看見,它調用了SYS_read。
- 回到/kernel/syscall.c,我們可以看見syscall_read的具體定義。
- 跳轉,我們會發現,調用了fileread這個函數,繼續跳轉
- 在這里,會調用一個至關重要的函數,就是read()
- 跳轉到這個函數里面,read就是我們讀取數據的關鍵函數
嗯。。這個函數還是蠻復雜的,先做下一個實驗吧
5. Xargs
我沒用過xargs,最開始可以說是一頭霧水,包括最開始做的時候,甚至還不知道可以傳遞多行參數,改了半天。
整體思路就是先將當前右側的參數讀取,然后循環從標準輸入中讀取數據,遇到換行符,則執行命令,然后重置當前的參數和緩沖區為初始狀態。
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/param.h"int main(int argc, char *argv[]) {if (argc < 2) {fprintf(2, "Usage: xargs command [args...]\n");exit(1);}char *cmd = argv[1];char *args[MAXARG];int i, n = 0;// 復制參數for (i = 1; i < argc && n < MAXARG - 1; i++) {args[n++] = argv[i];}int end = n;//方便重置索引char buf[512];int m = 0;while (read(0, &buf[m], 1) == 1) {if (buf[m] == '\n') {buf[m] = 0;args[n++] = &buf[0];// 參數必須以 NULL 結尾args[n] = 0;int fd = fork();if (fd == 0) {exec(cmd, args);fprintf(2, "xargs: exec failed\n");exit(1);}wait(0);// 索引重置m = 0;n = end;} else {m++;}}exit(0);
}
lab1給我感覺倒是沒有太多關于os的知識,更多的是需要你去熟悉這個xv6的大體是什么樣的,給他添加一些組件。
目前來看,收獲更多的還得是xv6的教科書,那里面確實能夠學到很多東西,給我的感覺就是比其他我看過的任何操作系統課對小白都要友好得多。