MIT6.S081/6.828 實驗1:Lab Utilities


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM