SHELL运行流程是怎么样的

75次阅读
没有评论

共计 6880 个字符,预计需要花费 18 分钟才能阅读完成。

这篇文章给大家分享的是有关 SHELL 运行流程是怎么样的的内容。丸趣 TV 小编觉得挺实用的,因此分享给大家做个参考,一起跟随丸趣 TV 小编过来看看吧。

一. 启动过程

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 函数设置选项的值;绑定 $ 位置参数的值), 然后根据不同的启动参数进入以下不同分支:

如果是只进行参数扩展而不执行命令,调用 run_wordexp 函数扩展参数,然后调用 exit_shell (last_command_exit_value) 函数以上次命令执行的返回值为返回值退出。

如果是以 - c 参数模式启动 shell,分为两种情况:一:如果是附带了字符串参数作为要执行的命令,则调用 run_one_command (command_execution_string) 执行 - c 附带的命令,参数 command_execution_string 保存 - c 后面附带的字符串命令值。执行完毕后调用 exit_shell (last_command_exit_value) 退出。二:如果是期待用户输入要执行的命令,则跳转到分支 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_builtin 和 execute_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 }
};

感谢各位的阅读!关于“SHELL 运行流程是怎么样的”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-16发表,共计6880字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)