2019独角兽企业重金招聘Python工程师标准>>>

版权声明:本文由李航原创文章,转载请注明出处: 
文章原文链接:https://www.qcloud.com/community/article/109

来源:腾云阁 https://www.qcloud.com/community

本文的目的:分享一下在学校的时候分析shell源码的一些收获,帮助大家了解shell的一个工作流程,从软件设计的角度,看看shell这样一个历史悠久的软件的一些设计优点和缺陷。本文重点不是讲SHELL语法,相信很多同事玩shell都很熟了。

本文的局限:限于本人技术水平和时间,肯定有不少错误和遗漏的地方,在当时的源码注释的过程中,也确实会有一直都不理解和存疑的地方,还请指正。但总的来说,主要逻辑和流程还是可以理清的。

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

bash代码简介:之前做过一个统计,shell源码大概有10万行,其中核心逻辑在1万多行,这也是分析的目标代码。剩下的包括引入的readline库(也是个开源库,处理输入的),yacc语法分析器生成工具(开源库,相信很多学过编译原理的都知道这东西),以及很多为提高用户界面友好性做得优化和辅助代码(比如!的历史操作)。

建议:在了解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。

  3. shell_initialized置为1表示shell初始化完成。调用eval.c中定义的函数reader_loop()不断的读取和解析用户输入,如果reader_loop函数返回,则调用exit_shell(last_command_exit_value)退出shell。

二.命令解析和执行流程

1. 主要相关文件

Eval.cCommand.hCopy_cmd.cExecute_cmd.cMake_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;#endifstruct 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.cvariables.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 }};

转载于:https://my.oschina.net/u/2987407/blog/779329

Shell主要逻辑源码级分析(1)——SHELL运行流程相关推荐

  1. fork的黑科技,它到底做了个啥,源码级分析linux内核的内存管理

    最近一直在学习linux内核源码,总结一下 https://github.com/xiaozhang8tuo/linux-kernel-0.11 一份带注释的源码,学习用. fork的黑科技,它到底做 ...

  2. 【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

  3. JDK1.8HashMap源码级分析

    任何不谈论JDK版本的HashMap介绍不是小白就是在耍流氓,所以本文是基于JDK1.7版本的HashMap分析,其中涉及到了面试常问的问题以及核心方法的源码分析 JDK1.8HashMap 红黑树前 ...

  4. 彻底搞懂equals以及hashCode方法(源码级分析)

    在开发过程中,我们经常会遇到要重写equals方法和重写hashCode方法的情况,那么,我们为什么要重写这个两个方法呢?重写这个两个方法有什么实际的作用吗?先别急,我们一一来分析,先看equals方 ...

  5. 从源码角度分析MapReduce的map-output流程

    文章目录 前言 流程图 源码分析 1 runNewMapper方法 2.NewOutputCollector方法 2.1 createSortingCollector方法 2.1.1 collecto ...

  6. 从源码角度分析MapReduce的reduce流程

    文章目录 前言 流程图 Reduce都干了哪些事? 源码分析 1.run方法 1.1 比较器getOutputValueGroupingComparator 1.1.1 getOutputKeyCom ...

  7. 微软AJax.net源码初步分析(2)--服务执行流程

    我以一个最简单的helloworld为例,演示AJax.net源码中调用后台服务的机制,只是列出一些大体的框架,具体细节我还在研究中:) 不当之处,欢迎指正. 我先把例子中的核心代码列出,方便大家阅读 ...

  8. 如何使用Easy-POI导入复杂表头的Excel报表(含源码级分析)

    本文旨在解决使用Easy-POI导入复杂表头(导出)时,遇上的EXCEL文件中的字段无法正常映射到Bean的问题,官方的示例和其他的CSDN文章基本上没有很详细的教程,于是自己写一个 这是需要导入的表 ...

  9. Spring IOC学习心得之源码级分析ContextLoaderListener的作用(IOC容器初始化入口)

    ContextLoaderListener类是负责初始化IOC容器,即在我们的web项目中,这里就是IOC容器初始化的入口,由这个类启动IOC容器的初始化. 它配置在web.xml中,比如如下配置: ...

最新文章

  1. 动态规划(一)简单例子
  2. 模块隐藏(LDR_MODULE链 与 PE特征)
  3. 请先设置tkk_物联卡apn设置机型大全!!物联卡连不上4G、网速慢?推荐你看这片文...
  4. python怎么修改while循环类型_python 的for与while 的i改变
  5. boost::hana::flip用法的测试程序
  6. 2.1 CPU 上下文切换(上)
  7. vim(三)golang代码跳转配
  8. 剑指Offer:二进制中1的个数
  9. Hive启动的三种方式
  10. [BZOJ 3709] Bohater
  11. mongodb占内存过大情况处理
  12. C# DataTable.Rows.Add(DataRow) 该行已经属于另一个表
  13. 医院药房管理系统 php,his 源码 医院管理系统
  14. NSA机密文件泄密者如何暴露身份
  15. 多元均值不等式的简单证明(调和平均数与算术平均数)
  16. 中央广播电视大学中等专业办公设备使用与维护
  17. 如何一个办公室里共享一个打印机,局域网设置打印机共享步骤。超简单,不懂技术都可操作
  18. revit模型怎么在手机上看_模型的查看和定位-Revit基础教程
  19. 从零开始的DIY智能家居 - 基于 ESP32 的土壤湿度传感器
  20. 萌卡纳我书飞翔公益捐书 守护阅读梦

热门文章

  1. 一篇文章搞懂JavaScript运行机制
  2. Python创建daemon
  3. Java IO编程全解(五)——AIO编程
  4. 软件工程实践第二次作业——个人项目实战(数独)
  5. [COCI 2013/2014 ROUND 3] parovi
  6. H3C设备之RIP v2认证
  7. ThreadPool基础之RegisterWaitForSingleObject
  8. Ubuntu服务器版硬件认证详情
  9. luvit 被忽视的lua 高性能框架(仿nodejs)
  10. HDU 5119 Happy Matt Friends ——(背包DP)