MIT6.S081/6.828 實驗1:Lab Unix Utilities

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分支,便可開始編寫咱們的程序。下面主要提供實現思路,具體實驗代碼請參考Githubgithub

實驗完成後使用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工做狀態的切換:用戶態 -> 內核態 -> 用戶態。

實驗代碼:https://github.com/zhayujie/xv6-riscv-fall19

原文連接:https://zhayujie.com/mit6828-lab-util.html

相關文章
相關標籤/搜索