共计 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 运行流程是怎么样的”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!