出处:
《跟老男孩学Linux:Shell编程实战》
目录
子shell的知识及实践说明
什么是子shell
子shell的本质可以理解为shell的子进程,子进程的概念是由父进程的概念引申而来的,在Linux系统中,系统运行的应用程序几乎都是从init(pid为1的进程)进程派生而来的,所有这些应用程序都可以视为init进程的子进程,而init则为它们的父进程,通过执行pstree -a命令就可以看到init及系统中其他进程的进程树信息。
子shell的常见产生途径及特点
1.带“&”提交后台作业
下面通过shell脚本来实现一个由“&”产生的子shell,脚本如下:
#! /bin/bash
parent_var="Parent"
#<==定义父shell变量parent_var并赋值标志性字符
echo "Shell Start: ParentShell Level: $BASH_SUBSHELL"
#<==输出父shell层级,BASH_SUBSHELL为系统环境变量
{
echo "SubShell Level: $BASH_SUBSHELL"
#<==输出子shell的层级,和父shell层级对比
sub_ver="Sub"
echo "parent_var=$parent_var"
sleep 2
echo "Subshell is over."
} & #<==由“&”产生的子shell
echo "Now ParentShell start again."
echo "Shell Over: ParentShell Level: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
echo "sub_var is not defined in ParentShell"
else
echo "sub_var is defined in ParentShell"
fi
Shell Start: ParentShell Level: 0
Now ParentShell start again.
Shell Over: ParentShell Level: 0
SubShell Level: 1
sub_var is not defined in ParentShell
parent_var=Parent
Subshell is over.
结论:
- 在shell中使用“&”可以产生子shell。
- 由“&”产生的子shell可以直接引用父shell定义的本地变量。
- 由“&”产生的子shell中定义的变量不能被父shell引用。
- 在shell中使用“&”可以实现其他程序的多线程并发功能。
2.使用“管道”功能
#! /bin/bash
parent_var="Parent"
echo "Shell Start: ParentShell Level: $BASH_SUBSHELL"
echo "" | \ #<==管道 换行
{
echo "SubShell Level: $BASH_SUBSHELL"
sub_ver="Sub"
echo "parent_var=$parent_var"
sleep 2
echo "Subshell is over."
} #<==去掉了&符号
echo "Now ParentShell start again."
echo "Shell Over: ParentShell Level: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
echo "sub_var is not defined in ParentShell"
else
echo "sub_var is defined in ParentShell"
fi
Shell Start: ParentShell Level: 0
SubShell Level: 1
parent_var=Parent
Subshell is over.
Now ParentShell start again.
Shell Over: ParentShell Level: 0
sub_var is not defined in ParentShell
结论:
- 在shell中使用管道可以产生子shell。
- 由管道产生的子shell可以直接引用父shell定义的本地变量。
- 由管道产生的子shell中定义的变量不能被父shell引用。
- 由管道产生的子shell不能异步执行,只能在执行完毕后才能返回到父shell环境。(在shell中使用管道不可以实现其他程序的多线程并发功能。)
3.使用“()”功能
#! /bin/bash
parent_var="Parent"
echo "Shell Start: ParentShell Level: $BASH_SUBSHELL"
( #<==使用小括号,将命令集括起来
echo "SubShell Level: $BASH_SUBSHELL"
sub_ver="Sub"
echo "parent_var=$parent_var"
sleep 2
echo "Subshell is over."
) #<==小括号结束
echo "Now ParentShell start again."
echo "Shell Over: ParentShell Level: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
echo "sub_var is not defined in ParentShell"
else
echo "sub_var is defined in ParentShell"
fi
Shell Start: ParentShell Level: 0
SubShell Level: 1
parent_var=Parent
Subshell is over.
Now ParentShell start again.
Shell Over: ParentShell Level: 0
sub_var is not defined in ParentShell
结论:
- 在shell中使用()可以产生子shell。
- 由()产生的子shell可以直接引用父shell定义的本地变量。
- 由()产生的子shell中定义的变量不能被父shell引用。
- 由()产生的子shell不能异步执行,只能在执行完毕后才能返回到父shell环境。(在shell中使用管道不可以实现其他程序的多线程并发功能。)
4.通过调用外部shell脚本产生子shell
父脚本
#! /bin/bash
parent_var="Parent"
export parent_env_var="Parent Env"
echo "Shell Start: ParentShell Level: $BASH_SUBSHELL"
sh ./20_1_4_SubShell.sh
sleep 1
echo "Now ParentShell start again."
echo "Shell Over: ParentShell Level: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
echo "sub_var is not definded in ParentShell"
else
echo "sub_var is defined in ParentShell"
fi
子脚本
#! /bin/bash
echo "SubShell Level: $BASH_SUBSHELL"
sub_var="Sub"
echo "sub_var=$sub_var"
echo "parent_var=$parent_var"
echo "parent_env_var=$parent_env_var"
sleep 2
echo "SubShell is over."
执行结果
Shell Start: ParentShell Level: 0
SubShell Level: 0
sub_var=Sub
parent_var= #<==子shell无法引用父shell变量,因此等号后面为空
parent_env_var=Parent Env #<==子shell可以引用父shell定义的全局环境变量
SubShell is over.
Now ParentShell start again.
Shell Over: ParentShell Level: 0
sub_var is not definded in ParentShell
结论:
- 调用外部shell脚本产生的子shell可以直接引用父shell定义的环境变量
- 调用外部shell脚本产生的子shell中定义的变量(或者环境变量)不能被父shell引用
- 调用外部shell脚本产生的子shell不能异步执行,只有执行完毕后才能返回到父shell环境
shell调用脚本的模式说明
在主脚本中嵌套脚本的方式有很多,常见的为fork、exec、source三种模式,这三种调用脚本的方式有一定的区别。
fork模式调用脚本知识
fork模式是最普通的脚本调用方式,及直接在父脚本里面用“/bin/bash /directory/script.sh”来调用脚本,或者在命令行中给script.sh脚本文件设置执行权限,然后使用/directory/script.sh来调用脚本。
使用上述方式调用脚本的时候,系统会开启一个SubShell(子shell)执行调用的脚本,SubShell执行的时候ParentShell还在,SubShell执行完毕后返回到ParentShell。最后的结论是SubShell可以从ParentShell继承环境变量,但是默认情况下SubShell中的环境变量不能带回ParentShell。
执行方式说明:
/directory/script.sh
#<== 对脚本赋予执行权限,直接执行脚本。
/bin/bash /directory/script.sh
#<== 在不赋予执行权限时,利用执行解释器执行
exec模式调用脚本
exec模式与fork模式调用脚本的方式不同,不需要新开一个SubShell来执行被调用的脚本。被调用的脚本与父脚本在同一个shell内执行,但是使用exec调用一个新脚本以后,父脚本中的exec执行之后的脚本内容就不会再执行了,这就是exec和source的区别。
执行方式说明:
exec /directory/script.sh
source模式调用脚本
source模式与fork模式的区别是不会新开一个SubShell来执行被调用的脚本,而是在同一个shell中执行,所以在被调用的脚本中声名的变量和环境变量都可以在主(父)脚本中获取和使用。
source模式与exec模式相比,最大的不同之处是使用source调用一个新脚本以后,父脚本中source命令行之后的内容在自脚本执行完毕后依然会被执行。
执行方式说明:
source /directory/script.sh
#<== 使用source不容易被误解,而“.”和“./”相近,容易被误解。
. /directory/script.sh
#<== “.”和source命令的功能是等价的。
对比fork模式与source模式的区别
对于fork模式,有以下结论:
- 父脚本执行后的PID信息与嵌套脚本(子shell脚本)执行后的PID信息不同,说明fork模式调用脚本确实产生了子shell
- 父脚本的变量信息会被嵌套如的脚本(子shell)引用,“SubShell.sh get$ParentVar=Parent”中等号右边的Parent即为调用父脚本变量后输出的结果。
- 在嵌入的脚本(子shell)中定义的变量信息无法被父脚本引用,“ParentShell.sh : Get : $SUB_VAR=”中等号右边内容为空,表示没有引用到父脚本的变量。
对于source模式,有以下结论:
- 父脚本执行后的PID信息与嵌套脚本(子shell脚本)执行后的PID信息一致,说明source模式调用脚本不会产生子shell,而是在同一个shell里执行,此项与fork模式不同。
- 在嵌入的脚本(子shell)中定义的变量信息可以被父脚本引用,“ParentShell.sh : Get: $SUB_VAR=Sub”中等号右边内容为Sub,表示引用了子shell脚本中的变量,此项与fork模式不同。
对比exec模式与source模式的区别
对于exec模式,有以下结论:
- 父脚本执行后的PID信息与嵌套脚本(子shell脚本)执行后的PID信息一致,说明exec模式调用脚本同不会产生子shell,而是在同一个shell里执行,此项与source模式相同
- 父脚本的变量信息会被嵌入的脚本(子shell脚本)引用,“SubShell.sh get$ParentVar=Parent”中等号右边的Parent即为调用父脚本变量后输出的结果,此项与fork及source模式都相同。
- 利用exec模式执行嵌入脚本(子shell脚本)的问题是,在执行完嵌入脚本后,紧接着嵌入脚本后的所有父脚本命令将不再执行,而是直接退出父脚本,此项与fork及source模式都不同。
shell调用脚本3种不同模式的应用场景
- fork模式调用脚本的应用场景
fork模式调用脚本主要应用于常规嵌套脚本执行的场合,嵌套的脚本只是执行相应的命令操作,不会生成相应的进程信息,父脚本不需要引用嵌套的脚本内的变量及函数等信息,其次在嵌套脚本中定义的变量及函数等不会影响到父脚本中相同的信息定义。 - exec模式调用脚本的应用场景
exec模式调用脚本需要应用于嵌套脚本在主脚本的末尾执行的场合,因此,此种模式的应用并不多见,并且可以被source模式完全取代。 - source模式调用脚本的应用场景
source模式调用脚本是比较重要且最常用的一种嵌套方式,主要应用之一是执行嵌套脚本启动某些服务程序。例如:在利用嵌套脚本启动Tomcat程序并生成PID程序文件时,如果选择fork模式,那么生成的PID文件信息就和执行“ps -ef”命令输出的PID信息不一致,这将会导致执行kill `cat tomcat_pid` 命令时,不能正确关闭Tomcat程序,而选择source模式就可以解决此问题。
source模式调用脚本的另外一个应用就是使得嵌套脚本中的变量及函数等信息被父脚本使用,从而实现更多的业务处理。