这篇文章给大家分享的是有关SHELL运行流程是怎么样的的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
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。
Eval.c Command.h Copy_cmd.c Execute_cmd.c Make_cmd.c
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不涉及到命令的解析,所以不在这里分析。
run_one_command (command_execution_string) 执行的过程中调用parse_and_execute
(在evalstring.c中定义)解析与执行命令,parse_and_execute
中实际调用execute_command_internal
函数进行命令的执行。
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
的执行上。
该函数是shell源码中执行命令的实际操作函数。他需要对作为操作参数传入的具体命令结构的value成员进行分析,并针对不同的value类型,再调用具体类型的命令执行函数进行具体命令的解释执行工作。
具体来说:如果value是simple,则直接调用execute_simple_command
函数进行执行,execute_simple_command
再根据命令是内部命令或磁盘外部命令分别调用execute_builtin
和execute_disk_command
来执行,其中,execute_disk_comman
d在执行外部命令的时候调用make_child
函数fork子进程执行外部命令。
如果value是其他类型,则调用对应类型的函数进行分支控制。举例来说,如果是value是for_commmand
,即这是一个for循环控制结构命令,则调用execute_for_command
函数。在该函数中,将枚举每一个操作域中的元素,对其再次调用execute_command
函数进行分析。即execute_for_command
这一类函数实现的是一个命令的展开以及流程控制以及递归调用execute_command
的功能。
因此,从main函数启动到命令执行的主要流程图可以表现为下图所示:
括号内为函数定义所在的文件。
variables.c variables.h
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
表示是整型类变量等等。
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变量,这保证了局部变量覆盖的正确语义。
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运行流程是怎么样的”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。