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

zhayujie發表於2020-06-07

Mit6.828/6.S081 fall 2019的Lab1是Unix utilities,主要內容為利用xv6的系統呼叫實現sleep、pingpong、primes、find和xargs等工具。本文對各程式的實現思路及xv6的系統呼叫流程進行詳細介紹。

前言

在實驗之前,推薦閱讀一下官網LEC1中提供的資料。其中Introduction是對該課程的的概述,examples則是幾個系統程式設計的樣例,這兩部分快速瀏覽一遍即可。對於xv6 book的第一章,則建議稍微細緻地閱讀一遍,特別是對fork()、exec()、pipe()、dup()這幾個系統呼叫的介紹,會在後面實驗中用到。

實驗環境搭建參考上一篇文章。進入xv6-riscv-fall19專案後可以看到兩個比較重要的目錄:kernel為xv6核心原始碼,裡面除了os工作的核心程式碼(如程式排程),還有向外提供的介面(system call);user中則是使用者程式,如我們熟悉的ls,echo命令等。本次實驗的目的就是在user中增加使用者程式,藉助kernel中提供的system call來實現所需的功能。

實驗思路

每一個Lab需要在對應的分支編寫程式碼,進入xv6-riscv-fall19目錄下,使用git checkout util切換到util分支,即可開始編寫我們的程式。下面主要提供實現思路,具體實驗程式碼請參考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))

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來對素數進行過濾。

每個程式收到的第一個數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

相關文章