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


博客中的文章均為meelo原創,請務必以鏈接形式注明本文地址

 

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

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

 

項目實現的shell

Shell介紹


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

命令行是一串ASCII字符由空格分隔。字符串的第一個單詞是一個可執行程序,或者是shell的內置命令。命令行的其余部分是命令的參數。如果第一個單詞是內置命令,shell會立即在當前進程中執行。否則,shell會新建一個子進程,然后再子進程中執行程序。新建的子進程又叫做作業。通常,作業可以由Unix管道連接的多個子進程組成。

如果命令行以&符號結尾,那么作業將在后台運行,這意味着在打印提示符並等待下一個命令之前,shell不會等待作業終止。 否則,作業在前台運行,這意味着shell在作業終止前不會執行下一條命令行。 因此,在任何時候,最多可以在一個作業中運行在前台。 但是,任意數量的作業可以在后台運行。例如,鍵入命令行:

sh> jobs

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

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、1、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM