深刻理解計算機系統項目之 Shell Lab

博客中的文章均爲meelo原創,請務必以連接形式註明本文地址html

 

Shell Lab是CMU計算機系統入門課程的一個實驗。在這個實驗裏你須要實現一個shell,shell是用戶與計算機的交互界面。普通意義上的shell就是能夠接受用戶輸入命令的程序。它之因此被稱做shell是由於它隱藏了操做系統低層的細節。完成Shell Lab你會對shell有更加深刻的認識,並熟悉Linux的多進程編程方法。linux

編程實現是一種絕佳的學習方式,然而就像這個實驗同樣,不少很好的課程做業都隱藏在互聯網當中。大多數人難以經過這種方式來學習,這篇文章的目的接就是介紹給你這個絕佳地學習Linux編程的方式,讓這個學習的過程變得稍微簡單一點。git

 

項目實現的shellgithub

Shell介紹shell


Shell會打印出提示符,等待來自stdlin的輸入,根據輸入執行特定地操做,這樣就產生了一種錯覺,彷佛輸入的文字(命令行)控制了程序的執行。編程

命令行是一串ASCII字符由空格分隔。字符串的第一個單詞是一個可執行程序,或者是shell的內置命令。命令行的其他部分是命令的參數。若是第一個單詞是內置命令,shell會當即在當前進程中執行。不然,shell會新建一個子進程,而後再子進程中執行程序。新建的子進程又叫作做業。一般,做業能夠由Unix管道鏈接的多個子進程組成。bash

若是命令行以&符號結尾,那麼做業將在後臺運行,這意味着在打印提示符並等待下一個命令以前,shell不會等待做業終止。 不然,做業在前臺運行,這意味着shell在做業終止前不會執行下一條命令行。 所以,在任什麼時候候,最多能夠在一個做業中運行在前臺。 可是,任意數量的做業能夠在後臺運行。例如,鍵入命令行:網絡

sh> jobsapp

會讓shell運行內置命令jobs。鍵入命令行ide

sh> /bin/ls -l -d

會致使shell在前臺運行ls程序。根據約定,shell會執行程序的main函數

int main(int argc, char *argv[])

argc和argv會接收到下面的值:

argc == 3,
argv[0] == ‘‘/bin/ls’’,
argv[1]== ‘‘-l’’,
argv[2]== ‘‘-d’’.

下面以&結尾的命令行會在後臺執行ls程序

sh> /bin/ls -l -d &

Unix shell支持做業控制的概念,容許用戶在前臺和後臺之間來回移動做業,並更改進程的狀態(運行,中止或終止)。在做業運行時,鍵入ctrl-c會將SIGINT信號傳遞到前臺做業中的每一個進程。SIGINT的默認動做是終止進程。相似地,鍵入ctrl-z會致使SIGTSTP信號傳遞給全部前臺進程。 SIGTSTP的默認操做是中止進程,直到它被SIGCONT信號喚醒爲止。Unix shell還提供支持做業控制的各類內置命令。例如:

jobs:列出運行和中止的後臺做業。
bg <job>:將中止的後臺做業更改成正在運行的後臺做業。
fg <job>:將中止或運行的後臺做業更改成在前臺運行。
kill <job>:終止做業。

 

實驗的流程


實驗和配套的教材《深刻理解計算機系統》是緊密相關的,在網絡上還能夠找到CMU使用這本教材的教學視頻。我沒有閱讀教材,只是把對應的視頻看了一遍。

實驗提供了初始文件,包括不少輔助函數,這樣你就只須要實現shell最爲核心的部分。

在作實驗以前,須要閱讀實驗說明,對實驗的總體有一個初步的認識。也就是說你須要瞭解你須要實現什麼功能,大致會須要什麼樣的函數。

你須要編寫的文件是tsh.c,所以須要把這個文件裏的程序閱讀一遍,瞭解提供了哪些輔助函數。

此外實驗還提供了測試用例以及標準的shell實現,這樣你就能夠對比你的實現結果是否與標準的結果一致。這是一個絕佳的調試方法,也是攻破這個實驗的一條路徑,先解決第1個測試用例,而後第2個……這樣你就不用擔憂無從下手了。

測試函數調用了myint、myspin和mysplit程序,所以你也須要閱讀一遍。

 

難點


編程須要遵循良好的編程規範,其中一個就是檢查函數的返回值,一般系統函數會使用返回值0或-1表示執行錯誤。雖然大多數狀況下都不會出現問題,可是一旦出錯檢查返回值可以讓你快速發現錯誤的源頭。在csapp.h頭文件裏,不少系統函數有一個頭文件大寫的函數,與原有的系統函數擁有一樣的參數,可是合理地檢查了返回值。

 

在fork新的進程時,有可能發生競爭條件。子進程很快結束了運行,發送SIGCHLD給主進程,進而回收子進程同時從做業列表中刪除該做業。可是此時,主進程還沒來得及將做業加入做業列表。解決方案是在主進程將做業加入做業列表以前屏蔽該信號,完成後再恢復該信號。須要注意的是子進程會繼承屏蔽的信號,所以在子進程也須要恢復。

 

另外一個難點是SIGCHLD的信號處理函數,若是你沒有正確處理,有可能會沒法經過最後一個測試用例。

問題之一:有可能多個子進程結束,主進程卻只接收到一次信號。主進程沒法知道有多少個子進程結束了。

解決方案:將waitpid置於while循環中,並傳入參數WNOHANG。參數WNOHANG表示,若是沒有須要回收的進程了,會返回0,若是回收了子進程會返回子進程的pid,經過判斷返回值就能夠結束循環。

問題之二:waitpid默認只有當正常結束纔會返回,若是是被其它進程kill或中止是不會返回的,這樣shell就無從知曉子進程是否結束了。

解決方案:傳入WUNTRACED參數給waitpid。這樣子進程正常結束、被kill或者是中止都會返回。咱們就須要一種方式判斷子進程究竟是由何種方式結束的,這個信息能夠在waitpid的status參數中獲得。status是一個整數,不一樣的值表示不一樣的返回狀態,有一系列的宏能夠判斷是否status是某種狀態。好比WIFEXITED(status)能夠判斷是否正常結束,WIFSIGNALED(status)能夠判斷是否被終止,WIFSTOPPED是否被中止。全部的信息均可以在man頁面找到。

 

實驗的說明裏提到,前臺進程與後臺進程的惟一區別是shell會等待前臺進程,所以前臺進程只有一個。waitfg實現了這一等待的功能。最顯而易見的選擇是用waitpid等待前臺進程的結束。那麼你須要像SIGCHLD信號處理函數那樣考慮各類複雜的進程結束條件,所以這不是最佳選擇。最佳選擇是使用sleep函數,只要前臺進程仍然是須要等待的進程,主進程就sleep。那麼sleep多長時間呢,sleep(0)是最佳的選擇,0表示進程會讓其它進程來執行,若是沒有其它的進程在執行會繼續執行。這樣總會有進程再執行,而不會出現CPU空轉的狀況。

 

從實驗說開去


從實驗中咱們明確區分了兩類命令:內置命令和可執行程序。內置命令直接執行,不須要進行做業管理,可執行程序須要建立一個可執行程序來執行。那麼對於一個真實的shell來講,有哪些內置命令。下面列出來bash的部份內置命令。shell內置命令大體能夠分爲4類,經過type命令能夠顯示命令的類型,type本身就是一個內置命令:

A.2.1  bash內置命令
.:執行當前進程環境中的程序。同source。
. file:dot命令從文件file中讀取命令並執行。
: 空操做,返回退出狀態0。
alias:顯示和建立已有命令的別名。
bg:把做業放到後臺。
bind:顯示當前關鍵字與函數的綁定狀況,或將關鍵字與readline函數或宏進行綁定。
break:從最內層循環跳出。
builtin [sh-builtin [args]]:運行一個內置Shell命令,並傳送參數,返回退出狀態0。當一個函數與一個內置命令同名時,該命令將頗有用。
cd [arg]:改變目錄,若是不帶參數,則回到主目錄,帶參數則切換到參數所指的目錄。
command comand [arg]:即便有同名函數,仍然執行該命令。也就是說,跳過函數查找。
declare [var]:顯示全部變量,或用可選屬性聲明變量。
dirs:顯示當前記錄的目錄(pushd的結果)。
disown:從做業表中刪除一個活動做業。
echo [args]:顯示args並換行。
enable:啓用或禁用Shell內置的命令。
eval [args]:把args讀入Shell,並執行產生的命令。
exec command:運行命令,替換掉當前Shell。
exit [n]:以狀態n退出Shell。
export [var]:使變量可被子Shell識別。
fc:歷史的修改命令,用於編輯歷史命令。
fg:把後臺做業放到前臺。
getopts:解析並處理命令行選項。
hash:控制用於加速命令查找的內部哈希表。
help [command]:顯示關於內置命令的有用信息。若是指定了一個命令,則將顯示該命令的詳細信息。
history:顯示帶行號的命令歷史列表。
jobs:顯示放到後臺的做業。
kill [-signal process]:向由PID號或做業號指定的進程發送信號。輸入kill-l查看信號列表。
let:用來計算算術表達式的值,並把算術運算的結果賦給變量。
local:用在函數中,把變量的做用域限制在函數內部。
logout:退出登陸Shell。
popd:從目錄棧中刪除項。
pushd:向目錄棧中增長項。
pwd:打印出當前的工做目錄。
read [var]:從標準輸入讀取一行,保存到變量var中。
readonly [var]:將變量var設爲只讀,不容許重置該變量。
return [n]:從函數中退出,n是指定給return命令的退出狀態值。
set:設置選項和位置參量。
shift [n]:將位置參量左移n次。
stop pid:暫停第pid號進程的運行。
suspend:終止當前Shell的運行(對登陸Shell無效)。
test:檢查文件類型,並計算條件表達式。
times:顯示由當前Shell啓動的進程運行所累計用戶時間和系統時間。
trap [arg] [n]:當Shell收到信號n(n爲0、一、2或15)時,執行arg。
type [command]:顯示命令的類型,例如:pwd是Shell的一個內置命令。
typeset:同declare。設置變量並賦予其屬性。
ulimit:顯示或設置進程可用資源的最大限額。
umask [八進制數字]:用戶文件關於屬主、屬組和其餘用戶的建立模式掩碼。
unalias:取消全部的命令別名設置。
unset [name]:取消指定變量的值或函數的定義。
wait [pid#n]:等待pid號爲n的後臺進程結束,並報告它的結束狀態。
meelo

處理做業:bg fg jobs disown kill wait stop

文件系統:cd pwd dirs pushd popd

變量相關:let local readonly printf var declare

命令相關:history type alias help unalias hash

函數相關:return shift

 

用實現的shell執行程序,必須給出程序的完整路徑,好比須要執行ls須要輸入/bin/ls。那麼bash是如何肯定該執行那個程序的呢?下面給出的兩篇文章解釋得很是清楚。shell會以必定的順序搜索命令,若是找到了命令就執行,沒找到會返回錯誤信息。

shell搜索變量的順序

  1. ALIASES
  2. Shell函數
  3. 內置命令
  4. HASH表
  5. PATH變量

https://www.cyberciti.biz/tips/how-linux-or-unix-understand-which-program-to-run-part-i.html

https://www.cyberciti.biz/tips/an-example-how-shell-understand-which-program-to-run-part-ii.html

相關文章
相關標籤/搜索