Mit6.828/6.S081 fall 2019的Lab1是Unix utilities,主要內容爲利用xv6的系統調用實現sleep、pingpong、primes、find和xargs等工具。本文對各程序的實現思路及xv6的系統調用流程進行詳細介紹。html
前言
在實驗以前,推薦閱讀一下官網LEC1中提供的資料。其中Introduction是對該課程的的概述,examples則是幾個系統編程的樣例,這兩部分快速瀏覽一遍便可。對於xv6 book的第一章,則建議稍微細緻地閱讀一遍,特別是對fork()、exec()、pipe()、dup()這幾個系統調用的介紹,會在後面實驗中用到。linux
實驗環境搭建參考上一篇文章。進入xv6-riscv-fall19項目後能夠看到兩個比較重要的目錄:kernel爲xv6內核源碼,裏面除了os工做的核心代碼(如進程調度),還有向外提供的接口(system call);user中則是用戶程序,如咱們熟悉的ls,echo命令等。本次實驗的目的就是在user中增長用戶程序,藉助kernel中提供的system call來實現所需的功能。git
實驗思路
每個Lab須要在對應的分支編寫代碼,進入xv6-riscv-fall19目錄下,使用git checkout util
切換到util分支,便可開始編寫咱們的程序。下面主要提供實現思路,具體實驗代碼請參考Github。github
實驗完成後使用make grade
能夠執行單元測試進行評分,會以gdb-server模式啓動qemu,並在gradelib.py中模擬gdb-client對咱們的程序進行測試。若是在make grade時報錯Timeout! Failed to connect to QEMU
,能夠將gradelib.py的325行改成self.sock.connect(("127.0.0.1", port))
。shell
sleep
sleep功能爲使進程睡眠若干個時鐘週期(xv6中一個tick爲100ms),首先建立user/sleep.c源文件,引入user.h頭文件,系統調用和工具函數都定義在該文件裏。核心代碼以下:編程
sleep(atoi(argv[1]));
完成編寫後,在Makefile的UPROGS中追加一行$U/_sleep\
。輸入make qemu
進行編譯,成功後進入shell,輸入sleep 10
,若是進程睡眠了大約1s,則表示程序編寫正確。數組
pingpong
功能是父進程經過管道向子進程發送1字節,子進程收到後向父進程回覆1字節。函數
因爲管道是單向流動的,因此兩次調用pipe()
建立兩個管道,分別對應兩個方向。使用fork()
建立子進程,在子進程中先從管道1read()
再向管道2write()
,父進程中則與之相反。工具
primes
primes的功能是輸出2~35之間的素數,實現方式是遞歸fork進程並使用管道連接,造成一條pipeline來對素數進行過濾。oop
每一個進程收到的第一個數p必定是素數,後續的數若是能被p整除則之間丟棄,若是不能則輸出到下一個進程,詳細介紹可參考文檔。僞代碼以下:
void primes() { p = read from left // 從左邊接收到的第一個數必定是素數 if (fork() == 0): primes() // 子進程,進入遞歸 else: loop: n = read from left // 父進程,循環接收左邊的輸入 if (p % n != 0): write n to right // 不能被p整除則向右輸出 }
還須要注意兩點:
-
文件描述符溢出: xv6限制fd的範圍爲0~15,而每次pipe()都會建立兩個新的fd,若是不及時關閉不須要的fd,會致使文件描述符資源用盡。這裏使用重定向到標準I/O的方式來避免生成新的fd,首先close()關閉標準I/O的fd,而後使用dup()複製所需的管道fd(會自動複製到序號最小的fd,即關閉的標準I/O),隨後對pipe兩側fd進行關閉(此時只會移除描述符,不會關閉實際的file對象)。
-
pipeline關閉: 在完成素數輸出後,須要依次退出pipeline上的全部進程。在退出父進程前關閉其標準輸入fd,此時read()將讀取到eof(值爲0),此時一樣關閉子進程的標準輸入fd,退出進程,這樣進程鏈上的全部進程就能夠退出。
find
find功能是在目錄中匹配文件名,實現思路是遞歸搜索整個目錄樹。
使用open()打開當前fd,用fstat()
判斷fd的type,若是是文件,則與要找的文件名進行匹配;若是是目錄,則循環read()到dirent結構,獲得其子文件/目錄名,拼接獲得當前路徑後進入遞歸調用。注意對於子目錄中的.
和..
不要進行遞歸。
xargs
xargs的功能是將標準輸入轉爲程序的命令行參數。可配合管道使用,讓本來沒法接收標準輸入的命令能夠使用標準輸入做爲參數。
根據lab中的使用例子能夠看出,xv6的xargs每次回車都會執行一次命令並輸出結果,直到ctrl+d時結束;而linux中的實現則是一直接收輸入,收到ctrl+d時才執行命令並輸出結果。
思路是使用兩層循環讀取標準輸入:
- 內層循環依次讀取每個字符,根據空格進行參數分割,將參數字符串存入二維數組中,當讀取到'\n'時,退出當前循環;當接收到ctrl+d(read返回的長度<0)時退出程序。
- 外層循環對每一行輸入
fork()
出子進程,調用exec()
執行命令。注意exec接收的二維參數數組argv,第一個參數argv[0]必須是該命令自己,最後一個參數argv[size-1]必須爲0,不然將執行失敗。
xv6系統調用流程
Lab中對system call的使用很簡單,看起來和普通函數調用並無什麼區別,但實際上的調用流程是較爲複雜的。咱們很容易產生一些疑問:系統調用的整個生命週期具體是什麼樣的?用戶進程和內核進程之間是如何切換上下文的?系統調用的函數名、參數和返回值是如何在用戶進程和內核進程之間傳遞的?
1.用戶態調用
在用戶空間,全部system call的函數聲明寫在user.h
中,調用後會進入usys.S
執行彙編指令:將對應的系統調用號(system call number)置於寄存器a7中,並執行ecall指令進行系統調用,其中函數參數存在a0~a5這6個寄存器中。ecall指令將觸發軟中斷,cpu會暫停對用戶程序的執行,轉而執行內核的中斷處理邏輯,陷入(trap)內核態。
2.上下文切換
中斷處理在kernel/trampoline.S
中,首先進行上下文的切換,將user進程在寄存器中的數據save到內存中(保護現場),並restore(恢復)kernel的寄存器數據。內核中會維護一個進程數組(最多容納64個進程),存儲每一個進程的狀態信息,proc結構體定義在proc.h,這也是xv6對PCB(Process Control Block)的實現。用戶程序的寄存器數據將被暫時保存到proc->trapframe
結構中。
3.內核態執行
完成進程切換後,調用trap.c/usertrap()
,接着進入syscall.c/syscall()
,在該方法中根據system call number拿到數組中的函數指針,執行系統調用函數。函數參數從用戶進程的trapframe
結構中獲取(a0~a5),函數執行的結果則存儲於trapframe的a0字段中。完成調用後一樣須要進程切換,先save內核寄存器到trapframe->kernel_*
,再將trapframe中暫存的user進程數據restore到寄存器,從新回到用戶空間,cpu從中斷處繼續執行,從寄存器a0中拿到函數返回值。
至此,系統調用完成,共經歷了兩次進程上下文切換:用戶進程 -> 內核進程 -> 用戶進程,同時伴隨着兩次CPU工做狀態的切換:用戶態 -> 內核態 -> 用戶態。