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

版權聲明:本文由李航原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/109shell

來源:騰雲閣 https://www.qcloud.com/community數據庫

本文的目的:分享一下在學校的時候分析shell源碼的一些收穫,幫助你們瞭解shell的一個工做流程,從軟件設計的角度,看看shell這樣一個歷史悠久的軟件的一些設計優勢和缺陷。本文重點不是講SHELL語法,相信不少同事玩shell都很熟了。數組

本文的侷限:限於本人技術水平和時間,確定有很多錯誤和遺漏的地方,在當時的源碼註釋的過程當中,也確實會有一直都不理解和存疑的地方,還請指正。但總的來講,主要邏輯和流程仍是能夠理清的。bash

分析的版本:首先選用最經常使用的bash,而後版本是bash4.2-release數據結構

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

建議:在瞭解shell運行機制的同時,從軟件設計的角度來看他,會發現有不少能夠優化和改進的地方(固然,由於shell自己是從比較久遠的年代發展而來,各類歷史因素相關),特別是,讀了下面內容的同窗應該能夠發現,命令解析那一塊,用C++的OO思想能夠合理的設計命令的類層次結構,大大簡化代碼量和邏輯,有興趣的同窗甚至能夠本身動手寫來試試替換掉這一部分。異步

一.啓動過程

shell.c是shell主函數main所在文件。所以shell的啓動能夠認爲從shell.c文件開始。main函數完成的主要工做流程是包括:檢查啓動的運行環境(是否經過sshd啓動,是否運行於emacs環境下,是否運行於cgywin環境下,是不是交互式shell,是不是login shell等,對系統進行內存泄露檢查,是不是受限shell),讀取配置文件(順序爲/etc/profile and~/.bash_profile OR ~/.bash_login OR ~/.profile)前面的存在不會讀後面的),設置運行須要的全局變量的值(當前環境變量、shell的名稱、啓動時間、輸入輸出文件描述符、語言本地化的相關設置),處理參數和選項(即帶有-c -s --debugger等參數和選項),設置參數和選項的值(run_shopt_alist ()函數調用shopt_setopt函數設置選項的值;綁定$位置參數的值),而後根據不一樣的啓動參數進入如下不一樣分支:函數

  1. 若是是隻進行參數擴展而不執行命令,調用run_wordexp函數擴展參數,而後調用exit_shell (last_command_exit_value)函數以上次命令執行的返回值爲返回值退出。工具

  2. 若是是以-c參數模式啓動shell,分爲兩種狀況:一:若是是附帶了字符串參數做爲要執行的命令,則調用run_one_command (command_execution_string)執行-c附帶的命令,參數command_execution_string保存-c後面附帶的字符串命令值。執行完畢後調用exit_shell (last_command_exit_value)退出。二:若是是期待用戶輸入要執行的命令,則跳轉到分支3。oop

  3. shell_initialized置爲1表示shell初始化完成。調用eval.c中定義的函數reader_loop()不斷的讀取和解析用戶輸入,若是reader_loop函數返回,則調用exit_shell(last_command_exit_value)退出shell。

二.命令解析和執行流程

1. 主要相關文件

Eval.c

Command.h

Copy_cmd.c

Execute_cmd.c

Make_cmd.c

2. shell命令結構:

shell中用以下結構體來表示一個命令。

typedef struct command {

  enum command_type type;   /* 命令的類型 */

  int flags;                /* 標記位,將影響命令的執行環境 */

  int line;                 /* 命令從哪一行開始 */

  REDIRECT *redirects;      /*關聯的重定向操做*/


  union {/*如下是一個聯合value,保存具體的「命令體」,多是for循環,case條件,

while循環等,union結構體的特徵是隻有一個值是有效的,所以如下命令種類是並列的,後

面有每一種命令類型的註釋*/

    struct for_com *For;

    struct case_com *Case;

    struct while_com *While;

    struct if_com *If;

    struct connection *Connection;

    struct simple_com *Simple;

    struct function_def *Function_def;

    struct group_com *Group;

#if defined (SELECT_COMMAND)

    struct select_com *Select;

#endif

#if defined (DPAREN_ARITHMETIC)

    struct arith_com *Arith;

#endif

#if defined (COND_COMMAND)

    struct cond_com *Cond;

#endif

#if defined (ARITH_FOR_COMMAND)

    struct arith_for_com *ArithFor;

#endif

    struct subshell_com *Subshell;

    struct coproc_com *Coproc;

  } value;

} COMMAND;

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

其中,通過對全部命令執行路徑的分析,肯定類型爲simple的command是通過命令替換後的最原子的命令操做,其他類型的命令都是由若干simple command構成的。

在shell啓動以後,不管是進入上面的2和3兩個分支中的哪個,最後解析命令所用到的函數都是execute_cmd.c中定義的函數。分支1不涉及到命令的解析,因此不在這裏分析。

3. 分支2的第一種狀況:

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

4. 分支2的第二種狀況和分支3:

reader_loop函數調用read_command函數解析命令,read_command函數調用parse_command()函數進行語法分析,parse_command()調用語法分析器y.tab.c中的yyparse()(該函數由yyac自動生成,所以再也不往函數內部跟進),將解析結果的命令字符串保存在全局變量GLOBAL_COMMAND中,而後執行execute_command函數(定義在execute_cmd.c中),execute_command函數再調用execute_command_internal函數進行命令的執行。至此分支2和分支3的狀況又合併到execute_command_internal的執行上。

5. execute_command_internal內部流程:

該函數是shell源碼中執行命令的實際操做函數。他須要對做爲操做參數傳入的具體命令結構的value成員進行分析,並針對不一樣的value類型,再調用具體類型的命令執行函數進行具體命令的解釋執行工做。

具體來講:若是value是simple,則直接調用execute_simple_command函數進行執行,execute_simple_command再根據命令是內部命令或磁盤外部命令分別調用execute_builtinexecute_disk_command來執行,其中,execute_disk_command在執行外部命令的時候調用make_child函數fork子進程執行外部命令。

若是value是其餘類型,則調用對應類型的函數進行分支控制。舉例來講,若是是value是for_commmand,即這是一個for循環控制結構命令,則調用execute_for_command函數。在該函數中,將枚舉每個操做域中的元素,對其再次調用execute_command函數進行分析。即execute_for_command這一類函數實現的是一個命令的展開以及流程控制以及遞歸調用execute_command的功能。

所以,從main函數啓動到命令執行的主要流程圖能夠表現爲下圖所示:

6. 從啓動到命令解釋的函數級流程圖:

括號內爲函數定義所在的文件。

三. 變量控制

1. 主要相關文件

variables.c

variables.h

2. 重要數據結構

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

變量上下文:上下文又能夠理解爲做用域,能夠比照C語言中的函數做用域,全局做用域來理解。一個上下文中的變量都是在這個上下文中可見的。
變量上下文結構定義:

typedef struct var_context {

  char *name;           /* name若是爲空則表示它存儲的是bash全局上下文,不然表示名爲name的函數的局部上下文*/

  int scope;         /*上下文在調用棧中的層數,0表明全局上下文 ,每深刻一層函數調用scope遞增1*/

  int flags;  /*標誌位集合flags記錄該上下文是否爲局部的、是否屬於函數、是否屬於內部命令,或者是否是臨時創建的等信息*/

  struct var_context *up; /* 指向函數調用棧中上一個上下文*/

  struct var_context *down;   /*指向函數調用棧中下一個上下文*/

  HASH_TABLE *table;    /* 同一上下文中的全部變量集合hash表,即名值對 */

} VAR_CONTEXT;

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

變量:bash中的變量不強調類型,能夠認爲都是字符串。其存儲結構以下

typedef struct variable {

  char *name;                  /*指向變量的名 */

  char *value;                  /*指向變量的值*/

  char *exportstr;            /*指向一個形如「名=值」的字符串*/

  sh_var_value_func_t *dynamic_value;    /* 若是是要返回一個動態值的函數,好比$SECONDS       或者$RANDOM,則函數指針指向生成該值的函數。*/

  sh_var_assign_func_t *assign_func; /* 若是是特殊變量被賦值時須要調用的回調函數,則其函數指針值保存在這裏

*/

  int attributes;         /* 只讀,可見等屬性*/

  int context;                    /*記錄該上下文變量屬於可訪問的做用域內局部變量
棧的哪一層*/

} SHELL_VAR;

因爲全部變量籠統的由字符串來表示,所以提供了attributes屬性成員來修飾變量的特性,好比屬性能夠是att_readonly表示只讀,att_array表示是數組變量,att_function表示是個函數,att_integer表示是整型類變量等等。

3. 做用機理

shell程序的執行伴隨着一個個上下文的切換,shell源碼中的變量控制也是基於這一點。將變量綁定於一個一個的上下文中。

舉例來講,一開始默認存在的是全局上下文,這裏稱爲global,其中包含有由main函數的參數或者配置文件傳入的變量值。若是這時進入了一個函數foo的執行中,則foo先從全局上下文獲取要導出的變量,加上本身新增的變量,構成foo的上下文局部變量,將foo的上下文壓入調用棧。這時調用棧看起來以下所示。

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

  • 棧底:global全局上下文(包含全部全局變量)

爲了解釋更詳細的狀況,假設在foo中又調用了fun函數,則fun先從foo中獲取要導出的變量,加上本身新增的變量,構成fun的上下文局部變量,而後將fun的上下文壓入調用棧的棧頂

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

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

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

  • 棧底:global全局上下文(包含全部全局變量)

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

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

  • 棧底:global全局上下文(包含全部全局變量)

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

4. 特殊變量:

bash中定義了若干特殊變量,特殊變量的意思是在該變量被修改後須要作一些額外的連貫工做。好比表示時區的變量TZ被修改了以後須要調用tzset函數修改系統中相應的時區設置。bash給這一類變量提供了一個回調函數接口,供其值發生改變的狀況下來調用該回調函數。這能夠類比數據庫中的觸發器機制。在bash中,特殊變量保存在一個全局數組special_vars中。其定義以下:

struct name_and_function {

            char *name;/*變量名*/

            sh_sv_func_t *function;/*變量值修改時要觸發的回調函數的函數指針*/

};

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

static struct name_and_function special_vars[] = {

  { "BASH_XTRACEFD", sv_xtracefd },

#if defined (READLINE)

#  if defined (STRICT_POSIX)

  { "COLUMNS", sv_winsize },

#  endif

  { "COMP_WORDBREAKS", sv_comp_wordbreaks },

#endif

  { "FUNCNEST", sv_funcnest },

  { "GLOBIGNORE", sv_globignore },

#if defined (HISTORY)

  { "HISTCONTROL", sv_history_control },

  { "HISTFILESIZE", sv_histsize },

  { "HISTIGNORE", sv_histignore },

  { "HISTSIZE", sv_histsize },

  { "HISTTIMEFORMAT", sv_histtimefmt },

#endif

#if defined (__CYGWIN__)

  { "HOME", sv_home },

#endif

#if defined (READLINE)

  { "HOSTFILE", sv_hostfile },

#endif

  { "IFS", sv_ifs },

  { "IGNOREEOF", sv_ignoreeof },

  { "LANG", sv_locale },

  { "LC_ALL", sv_locale },

  { "LC_COLLATE", sv_locale },

  { "LC_CTYPE", sv_locale },

  { "LC_MESSAGES", sv_locale },

  { "LC_NUMERIC", sv_locale },

  { "LC_TIME", sv_locale },

#if defined (READLINE) && defined (STRICT_POSIX)

  { "LINES", sv_winsize },

#endif

  { "MAIL", sv_mail },

  { "MAILCHECK", sv_mail },

  { "MAILPATH", sv_mail },

  { "OPTERR", sv_opterr },

  { "OPTIND", sv_optind },

  { "PATH", sv_path },

  { "POSIXLY_CORRECT", sv_strict_posix },

#if defined (READLINE)

  { "TERM", sv_terminal },

  { "TERMCAP", sv_terminal },

  { "TERMINFO", sv_terminal },

#endif /* READLINE */

  { "TEXTDOMAIN", sv_locale },

  { "TEXTDOMAINDIR", sv_locale },

#if defined (HAVE_TZSET) && defined (PROMPT_STRING_DECODE)

  { "TZ", sv_tz },

#endif

#if defined (HISTORY) && defined (BANG_HISTORY)

  { "histchars", sv_histchars },

#endif /* HISTORY && BANG_HISTORY */

  { "ignoreeof", sv_ignoreeof },

  { (char *)0, (sh_sv_func_t *)0 }

};
相關文章
相關標籤/搜索