自己動手實現一個UnixShell

NO IMAGE

這個實驗通過實現一個支持作業控制的Unix Shell,讓我們對進程控制和信號控制更加熟悉。課程Lab已經幫助我們搭建起了Shell的整體框架,並實現了與本次實驗不太相關的代碼,核心部分需要我們自己完成。

整體框架

Shell從標準輸入(stdin)讀取用戶輸入的命令,然後解析命令,Shell支持兩種類型的命令:如果用戶輸入的是的內置命令(如quitjobs等),那麼直接執行該命令;如果用戶輸入的是某個可執行文件的路徑,那麼通過fork一個子進程,在子進程中加載並執行命令。Shell把每次用戶輸入的命令抽象為一個job,一個job可以包含多個進程(例如管道)。每個job有兩種運行方式,如果用戶輸入的命令以’&‘結尾,那麼job將會在後臺(background)運行,否則,job運行在前臺(foreground)。在任意時刻,只允許存在01個前臺job,但是可以有0或多個後臺job運行。最後,為了支持用戶能夠向Shell發送信號,我們還需要實現3個信號處理程序,分別處理信號SIGCHLDSIGINTSIGTSTP

需要注意的地方

  • 默認的,一個子進程和它的父進程同屬於一個進程組,而Unix系統提供的大量向進程發送信號的機制,都是基於進程組這個概念的。當我們輸入Ctrl + C,內核會發送一個SIGINT信號到前臺進程組的每個進程,類似的,輸入Ctrl + Z會導致內核發送一個SIGTSTP信號給前臺進程組中的每個進程。這兒的“前臺進程組”指的是Shell進程所屬的進程組。實驗中,我們並不期望信號直接作用於Shell進程本身(否則Shell收到SIGINT信號就終止了),而是需要讓Shell將信號轉發給Shell前臺作業中的子進程及其所屬進程組中的所有進程。所以,我們不能讓子進程和Shell進程同屬一個進程組。具體做法是通過使用setpgid函數來改變子進程的進程組,當調用setpgid(0, 0)時,內核會創建一個新的進程組,其進程組ID是調用者進程的PID,並且會把調用者進程加入到這個進程組中。
  • Shell收到信號時,具體的工作需要信號處理函數來完成。例如收到SIGINT信號,那麼信號處理函數會把該信號發往前臺job中的進程及其所屬進程組中的所有進程。實驗中,我們是通過kill(pid_t pid, int sig)來發送信號,注意到我們並不僅僅是向PID = pid的進程發送信號,kill函數幫助我們實現了這一點:如果pid小於0kill發送信號sig給進程組|pid|pid的絕對值)中的每個進程。我們可以意識到,上一點需要注意的地方正是為這一點做鋪墊的。
  • 父進程(Shellfork了一個子進程後,父進程需要將這個進程作為一個job添加到job隊列中去(addjob),當子進程終止時,內核會發送一個SIGCHLD信號給父進程,然後在相應的信號處理程序中,把終止的子進程對應的jobjob隊列中刪除(deletejob)。考慮一種情況:當父進程fork了一個子進程之後,子進程先於父進程獲得調度,並且在父進程執行addjob前,子進程就已經終止了,併發送了SIGCHLD信號給父進程。此時,在信號處理程序中deletejob不會做任何操作,因為此時父進程還沒有把job加入到job隊列中。出現這個問題的根本原因是在addjob之前調用了deletejob。解決這個問題的方法是:在父進程fork子進程之前,將SIGCHLD信號阻塞,當完成addjob之後,才解除對SIGCHLD信號的阻塞,這樣就能保證在子進程被添加到job隊列之後再回收該子進程。注意,子進程繼承了它們父進程的被阻塞信號集合,所以我們必須在調用execve之前,解除子進程中阻塞的SIGCHLD信號。

代碼

Shell Lab的代碼在這裡

相關文章

React基礎(一)

如何實現一個WebServer

如何實現一個分佈式RPC框架

如何實現一個JavaClass解析器