天天看點

CS:APP3e 深入了解計算機系統_3e ShellLab(tsh)實驗

詳細的題目要求和資源可以到 http://csapp.cs.cmu.edu/3e/labs.html 或者 http://www.cs.cmu.edu/~./213/schedule.html 擷取。

Signal (IPC)

signal(2) - Linux manual page - man7.org

fork(2) - Linux manual page - man7.org

wait(2) - Linux manual page - man7.org

sigprocmask(2) - Linux manual page - man7.org

access(2) - Linux manual page - man7.org

sigemptyset(3): POSIX signal set operations

How to Use C's Volatile Keyword - Barr Group

Atomic Types

tsh的提示符為“tsh> ”

使用者的輸入分為第一個的<code>name</code>和後面的參數,之間以一個或多個空格隔開。如果<code>name</code>是一個tsh内置的指令,那麼tsh應該馬上處理這個指令然後等待下一個輸入。否則,tsh應該假設<code>name</code>是一個路徑上的可執行檔案,并在一個子程序中運作這個檔案(這也稱為一個工作、job)

tsh不需要支援管道和重定向

如果使用者輸入<code>ctrl-c</code> (<code>ctrl-z</code>),那麼<code>SIGINT</code> (<code>SIGTSTP</code>)信号應該被送給每一個在前台程序組中的程序,如果沒有程序,那麼這兩個信号應該不起作用。

如果一個指令以“&amp;”結尾,那麼tsh應該将它們放在背景運作,否則就放在前台運作(并等待它的結束)

每一個工作(job)都有一個正整數PID或者job ID(JID)。JID通過"%"字首辨別符表示,例如,“%5”表示JID為5的工作,而“5”代筆PID為5的程序。

tsh應該有如下内置指令:

tsh應該回收(reap)所有僵屍孩子,如果一個工作是因為收到了一個它沒有捕獲的(沒有按照信号處理函數)而終止的,那麼tsh應該輸出這個工作的PID和這個信号的相關描述。

利用測試檔案逐漸建構tsh,例如先從trace01.txt開始。

<code>setpgid</code>中的WUNTRACED and WNOHANG選項有用(參看前期準備)

當解析指令并産生子程序的時候(<code>fork</code> )的時候,必須先調用<code>sigprocmask</code> block <code>SIGCHLD</code>信号,調用<code>addjob</code>将剛剛建立的工作加入到工作清單裡,然後unblock該信号(課件裡有講這個競争産生的問題)。另外,由于子程序會繼承block的特性,是以子程序要記得unblock。

一些具有終端環境的程序會嘗試從父程序讀寫資料,例如/bin/sh,還有一些程式例如<code>more</code> <code>less</code> <code>vi</code> <code>emacs</code> 會對終端做一些“奇怪的設定”。本次實驗用<code>/bin/ls</code> <code>/bin/echo</code>這樣的文字模式的程式測試即可。

當我們在真正的shell(例如bash)中執行tsh時,tsh本身也是被放在前台程序組中的,它的子程序也會在前台程序組中,例如下圖所示:

是以當我們在終端輸入<code>ctrl-c</code> (<code>ctrl-z</code>)的時候,<code>SIGINT</code> (<code>SIGTSTP</code>)信号應該被送給每一個在前台程序組中的所有程序,包括我們在tsh中認為是背景程序的程式。一個決絕的方法就是在<code>fork</code>之後<code>execve</code>之前,子程序應該調用<code>setpgid(0, 0)</code>使得它進入一個新的程序組(其pgid等于該程序的pid)。tsh接收到<code>SIGINT</code> <code>SIGTSTP</code>信号後應該将它們發送給tsh眼中正确的“前台程序組”(包括其中的所有程序)。

我首先将書上(8.5.5節)說的6個關于信号處理函數安全性的要求列出(詳細的解釋請參考書),在程式設計的時候要注意:

盡量保持信号處理函數的簡單性,例如隻改變一個flag

在信号處理函數内部隻調用<code>async-signal-safe</code>的函數(<code>man 7 signal</code>裡面有完全的列出)

在進入和退出信号處理函數的時候儲存和還原<code>errno</code>變量(參考:Thread-local storage )

當試圖通路全局結構變量的時候暫時block所有的信号,然後還原

全局變量的聲明為<code>volatile</code>

将flag(标志)聲明為<code>sig_atomic_t</code>

下面我就實驗要求完成的7個函數說幾個注意的地方,代碼中的注釋也解釋了一些:

在調用<code>parseline</code>解析輸出後,我們首先判斷這是一個内置指令(shell實作)還是一個程式(本地檔案)。如果是内置指令,進入<code>builtin_cmd(argv, cmdline)</code> ,否則建立子程序并在job清單裡完成添加。這裡要注意在<code>fork</code>前用<code>access</code>判斷是否存在這個檔案,不然fork以後無法回收,另外要注意一個線程并行競争(race)的問題:<code>fork</code>以後會在job清單裡添加job,信号處理函數<code>sigchld_handler</code>回收程序後會在job清單中删除,如果信号來的比較早,那麼就可能會發生先删除後添加的情況。這樣這個job永遠不會在清單中消失了(記憶體洩露),是以我們要先block<code>SIGCHLD</code> ,添加以後再還原。

更新:<code>fork</code>子程序後發生錯誤退出子程序應該使用<code>_exit</code>而非<code>exit</code> (<code>unix_error</code>裡面也是用的<code>exit</code> ) 參考:What is the difference between using _exit() &amp; exit() in a conventional Linux fork-exec?

這個函數分情況判斷是哪一個内置指令,要注意如果使用者僅僅按下Enter鍵,那麼在解析後<code>argv</code>的第一個變量将是一個空指針。如果用這個空指針去調用<code>strcmp</code>函數會引發segment fault。

這個函數單獨處理了<code>bg</code>和<code>fg</code>這兩個内置指令。要注意<code>fg</code>有兩個對應的情況:1.背景程式是stopped的狀态,這時我們需要設定相關變量,然後發送繼續的信号。2.如果這個程序本身就在運作,我們就隻需要改變job的狀态,設定相關變量,然後進入<code>waitfg</code>等待這個新的前台程序執行完畢。

寫這個也出現了一個讓我debug 幾個小時的相容性問題:

在<code>man 7 signal</code>中,<code>SIGCHLD</code>描述如下:

也就是說,子程序終止或者停止的時候會向父程序發送這個信号,然後父程序進入<code>sigchld_handler</code>信号處理函數進行回收或者提示。但是在我的機器上卻發現在子程序從stopped變到running(收到<code>SIGCONT</code> )的時候也會向父程序發送這個信号。這樣就會出現一個問題:我們要使背景一個stopped的程序重新運作,但是它會向父程序(shell)發送一個<code>SIGCHLD</code> ,這樣父程序就會進入信号處理函數<code>sigchld_handler</code>試圖回收它(不是stop),而它有沒有結束,是以信号處理函數會一直等待它執行完畢,在shell中顯示的情況就是卡住了。

經過長時間調試确認後發現在POSIX某個标準中<code>SIGCHLD</code>信号的定義如下:

SIGCHLD The SIGCHLD signal is sent to a process when a child process terminates, is interrupted, or resumes after being interrupted. One common usage of the signal is to instruct the operating system to clean up the resources used by a child process after its termination without an explicit call to the <code>wait</code> system call.

<code>or resumes after being interrupted.</code> ,看到這句的時候我就要吐血了。。。

為了進一步證明我的想法,我在FreeBSD11.1上面查了一下手冊:

CS:APP3e 深入了解計算機系統_3e ShellLab(tsh)實驗

他說的是“changed”,看來我的機器是按照POSIX的某個标準實作的。

我的解決方案是設定一個<code>pid_t</code>的全局變量stopped_resume_child記錄我們要fg的stopped程序,在進入信号處理函數後首先檢查這個變量是否大于零,如果是就直接退出不做處理。(這裡其實有一個和其他程序競争的問題,時間有限就不去做更改了)

我之前聲明了一個<code>volatile sig_atomic_t</code>的全局變量<code>fg_pid_reap</code> ,隻要信号處理函數回收了前台程序,它就會将<code>fg_pid_reap</code> 置1,這樣我們的<code>waitfg</code>函數就會退出,接着讀取使用者的下一個輸入。使用busy<code>sleep</code>會有一些延遲,實驗報告上要求這麼實作我也沒辦法; )

注意儲存<code>errno</code> 。

注意到這裡不能使用while來回收程序,因為我們的背景還可能有正在運作的程序,這樣做的話會使得<code>waitpid</code>一直等待這個程序結束。當然使用if隻回收一次也可能會導緻信号累加的問題,例如多個背景程式同時結束,實驗報告上要求這麼實作我也沒辦法 ; )

注意如果程式是被stop的話<code>SIGTSTP ctrl-z</code> ,我們不用回收、删除job清單中的節點。

注意是群發,即<code>killpg</code>,不能隻發一個。

不解釋。

為了友善檢查結果,我寫了一個bash腳本,用來比較我的<code>tsh</code>和實驗給的正确參考程式<code>tshref</code>的輸出結果(測試用例為trace01.txt~trace16.txt):

全部列印出來太長,這裡列出最後幾個:

可以發現除了PID不同以外其餘都相同,說明<code>tsh</code>實作正确。

[完整項目代碼](https://files.cnblogs.com/files/liqiuhao/tsh.7z)

這次實驗給我最大的教訓就是不要完全相信文檔,自己去實作和求證也很重要。另外,并行産生的競争問題也有了一些了解。

另外,有意思的是,我在做實驗之前看到實驗指導裡說:

– In waitfg, use a busy loop around the sleep function. – In sigchld handler, use exactly one call to waitpid.

當時我還想說用<code>sleep</code> 和在<code>waitpid</code>裡面隻用一個回收是不是不安全或者太傻了,結果我上github一看不僅都是這樣,而且他們的代碼非常不安全(上面提到的六個安全注意點完全不遵守,各種調用也沒有檢查傳回值和異常),于是覺得自己寫的肯定比他們好多了

結果。。。如果注意這些安全問題會有很多麻煩,時間也有限,我就把幾個容易實作的實作了,還有兩個“通路全局結構變量前block”和“在信号處理函數中僅使用<code>async-signal-safe</code>沒有實作。

最後,改編一下Mutt E-Mail Client作者的一句話總結一下這次實驗:

All code about this ShellLab on github sucks. This one just sucks less 😉