自己動手實現一個Unix Shell

Chenyang發表於2017-01-10

這個實驗通過實現一個支援作業控制的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的程式碼在這裡

相關文章