這個實驗通過實現一個支援作業控制的Unix Shell
,讓我們對程式控制和訊號控制更加熟悉。課程Lab
已經幫助我們搭建起了Shell
的整體框架,並實現了與本次實驗不太相關的程式碼,核心部分需要我們自己完成。
整體框架
Shell
從標準輸入(stdin
)讀取使用者輸入的命令,然後解析命令,Shell
支援兩種型別的命令:如果使用者輸入的是的內建命令(如quit
、jobs
等),那麼直接執行該命令;如果使用者輸入的是某個可執行檔案的路徑,那麼通過fork
一個子程式,在子程式中載入並執行命令。Shell
把每次使用者輸入的命令抽象為一個job
,一個job
可以包含多個程式(例如管道)。每個job
有兩種執行方式,如果使用者輸入的命令以'&
'結尾,那麼job
將會在後臺(background
)執行,否則,job
執行在前臺(foreground
)。在任意時刻,只允許存在0
或1
個前臺job
,但是可以有0
或多個後臺job
執行。最後,為了支援使用者能夠向Shell
傳送訊號,我們還需要實現3
個訊號處理程式,分別處理訊號SIGCHLD
、SIGINT
和SIGTSTP
。
需要注意的地方
- 預設的,一個子程式和它的父程式同屬於一個程式組,而
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
小於0
,kill
傳送訊號sig
給程式組|pid|
(pid
的絕對值)中的每個程式。我們可以意識到,上一點需要注意的地方正是為這一點做鋪墊的。 - 父程式(
Shell
)fork
了一個子程式後,父程式需要將這個程式作為一個job
新增到job
佇列中去(addjob
),當子程式終止時,核心會傳送一個SIGCHLD
訊號給父程式,然後在相應的訊號處理程式中,把終止的子程式對應的job
從job
佇列中刪除(deletejob
)。考慮一種情況:當父程式fork
了一個子程式之後,子程式先於父程式獲得排程,並且在父程式執行addjob
前,子程式就已經終止了,併傳送了SIGCHLD
訊號給父程式。此時,在訊號處理程式中deletejob
不會做任何操作,因為此時父程式還沒有把job
加入到job
佇列中。出現這個問題的根本原因是在addjob
之前呼叫了deletejob
。解決這個問題的方法是:在父程式fork
子程式之前,將SIGCHLD
訊號阻塞,當完成addjob
之後,才解除對SIGCHLD
訊號的阻塞,這樣就能保證在子程式被新增到job
佇列之後再回收該子程式。注意,子程式繼承了它們父程式的被阻塞訊號集合,所以我們必須在呼叫execve
之前,解除子程式中阻塞的SIGCHLD
訊號。
程式碼
Shell Lab
的程式碼在這裡。