天天看點

Shell 主要邏輯源碼級分析:SHELL 運作流程 (1)

本文的目的:分享一下在學校的時候分析shell源碼的一些收獲,幫助大家了解shell的一個工作流程,從軟體設計的角度,看看shell這樣一個曆史悠久的軟體的一些設計優點和缺陷。本文重點不是講SHELL文法,相信很多同僚玩shell都很熟了。

本文的局限:限于本人技術水準和時間,肯定有不少錯誤和遺漏的地方,在當時的源碼注釋的過程中,也确實會有一直都不了解和存疑的地方,還請指正。但總的來說,主要邏輯和流程還是可以理清的。

分析的版本:首先選用最常用的bash,然後版本是bash4.2-release

bash代碼簡介:之前做過一個統計,shell源碼大概有10萬行,其中核心邏輯在1萬多行,這也是分析的目标代碼。剩下的包括引入的readline庫(也是個開源庫,處理輸入的),yacc文法分析器生成工具(開源庫,相信很多學過編譯原理的都知道這東西),以及很多為提高使用者界面友好性做得優化和輔助代碼(比如!的曆史操作)。

建議:在了解shell運作機制的同時,從軟體設計的角度來看他,會發現有很多可以優化和改進的地方(當然,因為shell本身是從比較久遠的年代發展而來,各種曆史因素相關),特别是,讀了下面内容的同學應該可以發現,指令解析那一塊,用C++的OO思想可以合理的設計指令的類層次結構,大大簡化代碼量和邏輯,有興趣的同學甚至可以自己動手寫來試試替換掉這一部分。

<code>shell.c</code>是shell主函數main所在檔案。是以shell的啟動可以認為從<code>shell.c</code>檔案開始。main函數完成的主要工作流程是包括:檢查啟動的運作環境(是否通過sshd啟動,是否運作于emacs環境下,是否運作于cgywin環境下,是否是互動式shell,是否是login shell等,對系統進行記憶體洩露檢查,是否是受限shell),讀取配置檔案(順序為<code>/etc/profile and</code>( <code>~/.bash_profile OR ~/.bash_login OR ~/.profile</code>)前面的存在不會讀後面的),設定運作需要的全局變量的值(目前環境變量、shell的名稱、啟動時間、輸入輸出檔案描述符、語言本地化的相關設定),處理參數和選項(即帶有<code>-c -s --debugger</code>等參數和選項),設定參數和選項的值(<code>run_shopt_alist ()</code>函數調用<code>shopt_setopt</code>函數設定選項的值;綁定$位置參數的值),然後根據不同的啟動參數進入以下不同分支:

如果是隻進行參數擴充而不執行指令,調用<code>run_wordexp</code>函數擴充參數,然後調用<code>exit_shell</code> (<code>last_command_exit_value</code>)函數以上次指令執行的傳回值為傳回值退出。

如果是以-c參數模式啟動shell,分為兩種情況:一:如果是附帶了字元串參數作為要執行的指令,則調用<code>run_one_command (command_execution_string)</code>執行-c附帶的指令,參數<code>command_execution_string</code>儲存-c後面附帶的字元串指令值。執行完畢後調用<code>exit_shell (last_command_exit_value)</code>退出。二:如果是期待使用者輸入要執行的指令,則跳轉到分支3。

将<code>shell_initialized</code>置為1表示shell初始化完成。調用<code>eval.c</code>中定義的函數<code>reader_loop()</code>不斷的讀取和解析使用者輸入,如果<code>reader_loop</code>函數傳回,則調用<code>exit_shell</code>、<code>(last_command_exit_value)</code>退出shell。

shell中用如下結構體來表示一個指令。

其中一個很關鍵的成員是聯合union類型value,它指出了該指令的類型,也給出了儲存指令具體内容的指針。從該結構的可選值來看,shell定義的指令共有for循環、case條件、while循環、函數定義、協同異步指令等14種。

其中,經過對所有指令執行路徑的分析,确定類型為simple的command是經過指令替換後的最原子的指令操作,其餘類型的指令都是由若幹simple command構成的。

在shell啟動之後,無論是進入上面的2和3兩個分支中的哪一個,最後解析指令所用到的函數都是<code>execute_cmd.c</code>中定義的函數。分支1不涉及到指令的解析,是以不在這裡分析。

run_one_command (command_execution_string) 執行的過程中調用<code>parse_and_execute</code> (在evalstring.c中定義)解析與執行指令,<code>parse_and_execute</code>中實際調用<code>execute_command_internal</code>函數進行指令的執行。

<code>reader_loop</code>函數調用<code>read_command</code>函數解析指令,<code>read_command</code>函數調用<code>parse_command()</code>函數進行文法分析,<code>parse_command()</code>調用文法分析器y.tab.c中的yyparse()(該函數由yyac自動生成,是以不再往函數内部跟進),将解析結果的指令字元串儲存在全局變量<code>GLOBAL_COMMAND</code>中,然後執行<code>execute_command</code>函數(定義在<code>execute_cmd.c</code>中),<code>execute_command</code>函數再調用<code>execute_command_internal</code>函數進行指令的執行。至此分支2和分支3的情況又合并到<code>execute_command_internal</code>的執行上。

該函數是shell源碼中執行指令的實際操作函數。他需要對作為操作參數傳入的具體指令結構的value成員進行分析,并針對不同的value類型,再調用具體類型的指令執行函數進行具體指令的解釋執行工作。

具體來說:如果value是simple,則直接調用<code>execute_simple_command</code>函數進行執行,<code>execute_simple_command</code>再根據指令是内部指令或磁盤外部指令分别調用<code>execute_builtin</code>和<code>execute_disk_command</code>來執行,其中,<code>execute_disk_comman</code>d在執行外部指令的時候調用<code>make_child</code>函數fork子程序執行外部指令。

如果value是其他類型,則調用對應類型的函數進行分支控制。舉例來說,如果是value是<code>for_commmand</code>,即這是一個for循環控制結構指令,則調用<code>execute_for_command</code>函數。在該函數中,将枚舉每一個操作域中的元素,對其再次調用<code>execute_command</code>函數進行分析。即<code>execute_for_command</code>這一類函數實作的是一個指令的展開以及流程控制以及遞歸調用<code>execute_command</code>的功能。

是以,從main函數啟動到指令執行的主要流程圖可以表現為下圖所示:

括号内為函數定義所在的檔案。

Shell 主要邏輯源碼級分析:SHELL 運作流程 (1)

BASH中主要通過變量上下文和變量兩個結構體來描述一個變量結構。以下分别介紹。

變量上下文:上下文又可以了解為作用域,可以比照C語言中的函數作用域,全局作用域來了解。一個上下文中的變量都是在這個上下文中可見的。

變量上下文結構定義:

描述一個變量的作用域的結構體。一個上下文中的所有變量,存放在var_context的table成員中。

變量:bash中的變量不強調類型,可以認為都是字元串。其存儲結構如下

由于所有變量籠統的由字元串來表示,是以提供了attributes屬性成員來修飾變量的特性,比如屬性可以是<code>att_readonly</code>表示隻讀,<code>att_array</code>表示是數組變量,<code>att_function</code>表示是個函數,<code>att_integer</code>表示是整型類變量等等。

shell程式的執行伴随着一個個上下文的切換,shell源碼中的變量控制也是基于這一點。将變量綁定于一個一個的上下文中。

舉例來說,一開始預設存在的是全局上下文,這裡稱為global,其中包含有由main函數的參數或者配置檔案傳入的變量值。如果這時進入了一個函數foo的執行中,則foo先從全局上下文擷取要導出的變量,加上自己新增的變量,構成foo的上下文局部變量,将foo的上下文壓入調用棧。這時調用棧看起來如下所示。

棧頂 :foo上下文(包含foo上下文的所有局部變量)

棧底:global全局上下文(包含所有全局變量)

為了解釋更詳細的情況,假設在foo中又調用了fun函數,則fun先從foo中擷取要導出的變量,加上自己新增的變量,構成fun的上下文局部變量,然後将fun的上下文壓入調用棧的棧頂

。這是調用棧看起來如下所示。

棧頂 :fun上下文(包含fun上下文的所有局部變量)

棧中 :foo上下文(包含foo上下文的所有局部變量)

此時假設fun函數執行完畢,則将fun上下文從棧中pop出,局部變量全部失效。調用棧又變成如下所示。

變量的查找順序:從棧頂往棧底,即如果棧頂上下文中沒有要查找的變量,則查找其在棧中的下一個上下文,如果整個調用棧查找完畢也沒有找到,則查找失敗。舉例來說,如果在棧頂上下文中有PWD變量(目前工作路徑),就不會去查找全局的PWD變量,這保證了局部變量覆寫的正确語義。

bash中定義了若幹特殊變量,特殊變量的意思是在該變量被修改後需要做一些額外的連貫工作。比如表示時區的變量TZ被修改了之後需要調用tzset函數修改系統中相應的時區設定。bash給這一類變量提供了一個回調函數接口,供其值發生改變的情況下來調用該回調函數。這可以類比資料庫中的觸發器機制。在bash中,特殊變量儲存在一個全局數組<code>special_vars</code>中。其定義如下:

該結構表示一個特殊變量結構,用于生成specialvars數組。回調函數一般是sv變量名的命名方式。