subShell与代码块

subShell 是一群被括在圆括号里的命令,这些命令会在另外的进程里执行。当你需要让一组命令在不同的目录下执行时,这种方法可以让你不修改主脚本的目录。

例; 将某个目录树通过管道复制到另外一个地方。

tar -cf - . | (cd /newdir; tar -xpf - )

代码块概念上与subShell相同,但是它不会建立新的进程。代码块里的命令用花括号{}括起来,且对主脚本的状态会产生影响。

 子shell: 每个shell脚本都有效地运行在父shell的一个子进程中. 这个父shell指的是在一个控制终端或在一个xterm窗口中给出命令提示符的那个进程.

shell脚本也能启动它自已的子进程. 这些子shell能够使脚本并行的, 有效的, 同时运行多个子任务.

圆括号中的命令列表( command1; command2; command3; ... )圆括号中命令列表的命令将会运行在一个子shell中.

子shell中的变量对于子shell之外的代码块来说, 是不可见的. 当然, 父进程也不能访问这些变量, 父进程指的是产生这个子shell的shell. 事实上, 这些变量都是局部变量.

子shell中的目录更改不会影响到父shell.

子shell中的目录更改不会影响到父shell.父shell不受任何影响, 并且父shell的环境也没有被更改.

子shell的另一个应用, 是可以用来检测一个变量是否被定义.

[[ ${variable-x} != x || ${variable-y} != y ]]  或 [[ ${variable-x} != x$variable ]]  或 [[ ${variable+x} = x ]]  或  [[ ${variable-x} != x ]]

使用"|"管道操作符, 将I/O流重定向到一个子shell中, 比如ls -al | (command).

在大括号中的命令不会启动子shell.{ command1; command2; command3; . . . commandN; }

 

 

Here Document:  一个here document就是一段带有特殊目的的代码段. 它使用I/O重定向的形式将一个命令序列传递到一个交互程序或者命令中, 比如ftp, cat, 或者ex文本编辑器.

  COMMAND <<InputComesFromHERE
          ...
  InputComesFromHERE

limit string用来界定命令序列的范围(译者注: 两个相同的limit string之间就是命令序列). 特殊符号<<用来标识limit string. 这个符号的作用就是将文件的输出重定向到程序或命令的stdin中.与interactive-program < command-file很相似, 其中command-file包含:
而here document看上去是下面这个样子:
  #!/bin/bash
  interactive-program <<LimitString
   command #1
   command #2
  ...
   LimitString
选择一个名字非常诡异limit string能够有效的避免命令列表与limit string重名的问题.

-选项用来标记here document的limit string (<<-LimitString), 可以抑制输出时前边的tab(不是空格). 这么做可以增加一个脚本的可读性.

结尾的limit string, 就是here document最后一行的limit string, 必须从第一个字符开始. 它的前面不能够有任何前置的空白. 而在这个limit string后边的空白也会引起异常.空白将会阻止limit string的识别.

 

列表结构: "与列表"和"或列表"结构能够提供一种手段, 这种手段能够用来处理一串连续的命令.

与列表: command-1 && command-2 && command-3 && ... command-n,如果每个命令执行后都返回true(0)的话, 那么命令将会依次执行下去. 如果其中的某个命令返
回false(非零值)的话, 那么这个命令链就会被打断, 也就是结束执行, (那么第一个返回false的命令, 就是最后一个执行的命令, 其后的命令都不会执行).

或列表:command-1 || command-2 || command-3 || ... command-n如果每个命令都返回false, 那么命令链就会执行下去. 一旦有一个命令返回true, 命令链就会被打断, 也就是结束执行, (第一个返回true的命令将会是最后一个执行的命令). 显然, 这和"与列表"完全相反.

与列表和或列表的退出状态码由最后一个命令的退出状态所决定。

false && true || echo false # false# 与下面的结果相同
( false && true ) || echo false # false
 # But *not* false && ( true || echo false ) # (没有输出)
#  注意, 以从左到右的顺序进行分组与求值,  这是因为逻辑操作"&&"和"||"具有相同的优先级.  最好避免这么复杂的情况, 除非你非常了解你到底在做什么.

 进程替换

进程替换与命令替换很相似. 命令替换把一个命令的结果赋值给一个变量, 比如dir_contents=`ls -al`或xref=$( grep word datafile). 进程替换把一个进程的输出提供给另一个进程(换句话说, 它把一个命令的结果发给了另一个命令).

 用圆括号扩起来的命令>(command) , <(command)启动进程替换. 它使用/dev/fd/<n>文件将圆括号中的进程处理结果发送给另一个进程.在"<"或">"与圆括号之间是没有空格的. 如果加了空格, 会产生错误

进程替换可以比较两个不同命令的输出, 甚至能够比较同一个命令不同选项情况下的输出.

comm <(ls -l) <(ls -al)diff <(ls $first_directory) <(ls $second_directory)cat <(ls -l)  # 等价于ls -l | catsort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)  # 列出系统3个主要'bin'目录中的所有文件, 并且按文件名进行排序.   注意是3个(查一下, 上面就3个圆括号)明显不同的命令输出传递给'sort'diff <(command1) <(command2) # 给出两个命令输出的不同之处.while read des what mask iface; do
 echo $des $what $mask $ifacedone < <(route -n)
一个更容易理解的等价代码是:
route -n |while read des what mask iface; do # 管道的输出被赋值给了变量. 
echo $des $what $mask $ifacedone #  这将产生出与上边相同的输出. 然而, Ulrich Gayer指出. . .这个简单的等价版本在while循环中使用了一个子shell,  因此当管道结束后, 变量就消失了.

 

 

Shell中脚本变量和函数变量的作用域   http://blog.csdn.net/ltx19860420/article/details/5570902

(1)Shell脚本中定义的变量是global的,其作用域从被定义的地方开始,到shell结束或被显示删除的地方为止。解析:脚本变量v1的作用域从被定义的地方开始,到shell结束。调用函数ltx_func的地方在变量v1的作用域内,所以能够访问并修改变量v1。

(2)Shell函数定义的变量默认是global的,其作用域从“函数被调用时执行变量定义的地方”开始,到shell结束或被显示删除处为止。函数定义的变量可以被显示定义成local的,其作用域局限于函数内。但请注意,函数的参数是local的。

解析:函数变量v2默认是global的,其作用域从“函数被调用时执行变量定义的地方”开始,到shell结束为止。注意,不是从定义函数的地方开始,而是从调用函数的地方开始。打印命令在变量v2的作用域内,所以能够访问变量v2。

 解析:函数变量v2显示定义为local的,其作用域局限于函数内。打印命令在函数外,不在变量v2的作用域内,所以能够不能访问变量v2。函数参数是local的,通过位置变量来访问。打印命令输出函数的第一个参数。

(3)如果同名,Shell函数定义的local变量会屏蔽脚本定义的global变量。

 在函数被调用之前, 所有在函数中声明的变量, 在函数体外都是不可见的,当然也包括那些被明确声明为local的变量.

退出状态码
函数返回一个值, 被称为退出状态码. 退出状态码可以由return命令明确指定, 也可以由函数中最后一条命令的退出状态码来指定(如果成功则返回0, 否则返回非0值). 可以在脚本中使用$?来引用退出状态码. 因为有了这种机制, 所以脚本函数也可以象C函数一样有"返回值".
return终止一个函数. return命令 [1]可选的允许带一个整型参数, 这个整数将作为函数的"退出状态码"返回给调用这个函数的脚本, 并且这个整数也被赋值给变量$?.函数所能返回最大的正整数是255.  为了让函数可以返回字符串或是数组, 可以使用一个在函数外可见的专用全局变量.

重定向函数的stdin函数本质上其实就是一个代码块, 这就意味着它的stdin可以被重定向.

 

2:shell什么情况下会产生子进程
 
以下几个创建子进程的情况。(以下英文摘自info bash)1:&,提交后台作业
If a command is terminated by the control operator `&', the shell executes the command asynchronously in a subshell.2:管道
Each command in a pipeline is executed in its own subshell3:括号命令列表
()操作符
     Placing a list of commands between parentheses causes a subshell
     environment to be created4:执行外部脚本、程序:
When Bash finds such a file while searching the `$PATH' for a command, it spawns a subshell to execute it.  In other words, executing                     filename ARGUMENTS
        is equivalent to executing
                   bash filename ARGUMENTS
 
说明:大致上子进程的创建包括以上四种情况了。需要说明的是只要是符合上边四种情况之一,便会创建(fork)子进程,不因是否是函数,命令,或程序,也不会因为是内置函数(buitin)或是外部程序。

   

shell退出后后台程序保持运行
 
在liunx上,如果想让一个进程在后台运行,最直接的方法是用&符号. 
比如ping www.baidu.com & 
 
但是这样一来,这个进程便成为了当前shell的一个job,在shell退出时,job会收到一个信号,也随之停止. 
处理这个问题,可以使用nohup命令,让job忽略shell的退出信号,也可以采用其他方式. 
 
既然后台命令作为shell的子进程会在shell退出时被杀掉,那么只要不让后台命令作为shell的子进程即可.比较简单的方式是利用subshell来调用后台命令. 
比如写个start.sh 
 
#!/bin/bash 
ping www.baidu.com & 
 
然后在shell中调用start.sh. 
 
./start.sh 
 
我猜测虽然start.sh是ping命令的父进程,但是start.sh运行完毕就退出了,ping命令成了孤儿进程,会被init进程收养.所以退出shell也不会导致ping命令中断

    让进程在后台可靠运行的几种方法
   

关于子 shell / 子进程 的讨论
from:http://bbs.chinaunix.net/viewthread.php?tid=733138&extra=&page=1
from:http://bbs.chinaunix.net/thread-1479880-2-1.html
from:http://tldp.org/LDP/abs/html/subshells.html
from:http://bbs.chinaunix.net/viewthread.php?tid=733138&extra=&page=1#
from:http://bbs.chinaunix.net/thread-742295-1-1.html
例子1、
cat sleep.sh
#!/bin/sh
sleep 100
[root@rhel-oracle shell]# ps -ef | grep sleep
root      9007  8947  1 05:10 pts/1    00:00:00 /bin/sh ./sleep.sh
root      9008  9007  0 05:10 pts/1    00:00:00 sleep 100
root      9010  8947  0 05:10 pts/1    00:00:00 grep sleep
[root@rhel-oracle shell]# echo $$
8947
[root@rhel-oracle shell]#
shell命令(sleep)就是执行它的这个终端的孙shell,sh的子shell
如果是脚本执行的,那命令就是执行它的这个脚本的shell的子shell
子shell其实就是当前shell启动的一个子进程,这个子进程也就是子shell,来执行shell脚本的语句,shell脚本里面的命令,都是由这个子shell来开启子进程
例子2、
cat a
echo $$
15:40:22#~> (./a.sh)
2140
15:40:28#~> ./a.sh
3292
15:40:30#~> echo $$
1248
15:40:38#~> (echo $$)
1248
15:40:42#~>
$$不能衍生到子shell的,
[root@localhost ~]# echo $$;(echo $$);
15788
15788
要用 bash4 的一个新的内置变量 $BASHPID 可以扩展到子进程
注:关于括号中的变量是否是变量替换导致的还是子进程执行的结果,请参考:
http://hi.baidu.com/leejun_2005/blog/item/6d0d9865433f8eeaf6365408.html
3、关于child process/subshell 的定义:
Definition: A subshell is a child process launched by a shell (or shell script).  #http://tldp.org/LDP/abs/html/subshells.html
A subshell does not create a new instance of the shell, just a new environment. #http://bbs.chinaunix.net/thread-1479880-2-1.html
按这句话的意思来说的话,岂不是所有子进程都由shell启动的?
我刚才说过嘛,是shell运行产生了进程(init和crontab呢?)
echo a|grep a这个echo和grep是子shell
a.sh里面调用,不是子shell (错了)
If  this  execution  fails  because the file is not in executable format, and the file is not a directory, it is assumed to be a shell script, a file containing shell commands.  A subshell is spawned to execute it.
子shell与子进程的区别:
子shell的话会保留之前的变量,子进程只会保留export的变量
14:28:07#tp#~> echo $BASHPID;(echo $BASHPID)
3918
5640
14:28:10#tp#~> echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL)
0
1
14:28:25#tp#~>
子shell就是由当前shell通过shell语法生成的某个进程,这些语法有
管道|
&
() 等等
而bash xxx.sh等方式是启用了一个新的子进程,变量不能被继承,除非 export 出来。
进程与子进程的讲法是从进程关系和进程树上做分析的,这个讲法搞C的人都比较好理解。
shell跟子shell,这个是shell编程的说法。
在info bash里提到子shell(sub shell)其实有以下四种方法:
1:管道。2:后台&,3:命令对列();4:执行外部脚本:filename ARGUMENTS
子进程, 不外乎就是fork+exec家族 
4、为啥说命令也是子shell
这个pid的ppid是刚才那个shell的pid
如果说你在终端执行了一个命令,那么这个命令是终端这个shell的子shell
如果脚本里执行了一个命令,这个命令是脚本的子shell
脚本本身也是个shell
我算是明白了
这其实涉及到clone和execve两个调用
shell中,外部命令后台调用时,会先fork一个子shell,然后用exec去执行这个命令,那么这个子shell也就消亡了,从ps中是看不到这个子shell存在的,因为被替换了
也不能说消亡,他所在进程依然存在
subshell存在的时间也就是fork之后exec之前?
嗯,我是这么想的
是的

5、运行下面三条命令,分析结果
a(){ sleep 30;}
a &
pstree -pa $$  
用rh,bash环境讨论呵  
bash,1248
  +-bash,2452
  |   +-sleep,1672 30
  +-pstree,288 -pa 1248
&生成一个子shell,#If  a  command  is  terminated by the control operator &, the shell executes the command in the background in a subshell.
函数生成一个子shell,
所以两个
有时间再看看这个,看是不是函数产生的进程,还是命令产生的进程:
a(){ while true ;do : ;done; }
a &
pstree -pa $$
对比一下
======================================================================================================
6、不過要提醒一下:
sub shell 也有分 sub shell 跟 nested sub shell 。
( ) 將 command group 置於 sub-shell 去執行,也稱 nested sub-shell。
sub shell a child shell executed under an other shell

7、几个知识点
1.Bash在实现pipeline(管道|)时会发起两个subshell(子shell)来运行|两边的命令,对于系统来说就是发起两个childprocess(子进程)
2.fork是产生process的唯一途径,exec*是执行程序的唯一途径
3.子进程会完全复制父进程,除了$PID与$PPID
4.fork子进程时继承父进程的进程名,在exec*执行命令时才由exec*替换为子进程对应的命令,同一进程的命令名可以由一个个exec*任意多次的改变
8、下面的脚本 get_process
    #!/bin/bash
    ps -ef|grep get_process
复制代码
运行./get_process后会得到什么结果
结果有三种
    waker     3260  3193  0 08:47 pts/1    00:00:00 /bin/bash ./get_process
复制代码
    waker     3290  3193  0 08:48 pts/1    00:00:00 /bin/bash ./get_process
    waker     3292  3290  0 08:48 pts/1    00:00:00 /bin/bash ./get_process
复制代码
    waker     3263  3193  0 08:48 pts/1    00:00:00 /bin/bash ./get_process
    waker     3265  3263  0 08:48 pts/1    00:00:00 grep get_process
复制代码
大家产生了种种解释
其实大家离真相只有0.01mm的距离
让我来提示一下
观察三种结果,都有一个共同的进程,就是PPID 是3193的 /bin/bash ./get_process
那么3193是什么
只要echo $$就可以看出3193就是我们键入 ./get_process的shell
让我们参照四个知识点来解释这个现象
首先当前shell 3193发起一个subshell来执行脚本./get_process
这个进程pid 3290 进程名 /bin/bash ./get_process
然后当它遇到管道,将发起两个子进程 3291(用来执行ps) 3192(用来执行grep),这两个进程是并行的
让我们给3192来个慢镜头
               史前时代                   fork                                       exec*(grep.*)
    pid          N/A                               3192
    进程名       N/A        /bin/bash ./get_process                  grep get_process
    说明         N/A       这个进程名继承自父进程                          exec*将进程名替换
复制代码

而用3291中的ps来观察这个进程的时候,由于两个子进程的并行,每一种状态都可能被ps看到,当然每次只能看到一种状态
所以结果会有三种情况
进程3290(/bin/bash ./get_process)
与下面三种可能的组合
1.空
2.子进程3292 进程名 /bin/bash ./get_process
3.子进程3292 进程名 grep get_process
9、子进程能传值给父进程吗?
不可以,但是可以变通下,source ./urcode.sh 或者管道、文件吧
10、命令、进程、子shell
Q1: shell如何执行“简单”命令?
A: 这里的简单命令和bash参考手册里的含义相同,形式上一般是:命令的名称加上它的参数。有三种不同的简单命令:
1.内置命令(builtin)
是shell解释程序内建的,有shell直接执行,不需要派生新的进程。有一些内部命令可以用来改变当前的shell环境,如:
cd /path
var=value
read var
export var
...
2.外部命令("external command" or "disk command")
二进制可执行文件,需要由磁盘装入内存执行。会派生新的进程,shell解释程序会调用fork自身的一个拷贝,然后用exec系列函数来执行外部命令,然后外部命令就取代了先前fork的子shell。
3.shell脚本(script)
shell解释程序会fork+exec执行这个脚本命令,在exec调用中内核会检查脚本的第一行(如:#!/bin/sh),找到用来执行脚本的解释程序,然后装入这个解释程序,由它解释执行脚本程序。解释程序可能有很多种,各种shell(Bourne shell,Korn shell cshell,rc及其变体ash,dash,bash,zshell,pdksh,tcsh,es...),awk,tcl/tk,expect,perl,python,等等。在此解释程序显然是当前shell的子进程。如果这个解释程序与当前使用的shell是同一种shell,比如都是bash,那么它就是当前shell的子shell,脚本中的命令都是在子shell环境中执行的,不会影响当前shell的环境。

Q2: shell脚本是否作为单独的一个进程执行?
A: 不是,shell脚本本身不能作为一个进程。如上面讲的,shell脚本由一个shell解释程序来解释、运行其中的命令。这个shell解释程序是单独的一个进程,脚本中的外部命令也都作为独立进程依次被运行。这也就是为什么ps不能找到正在运行的脚本的名字的原因了。作为一个替代方案,你可以这样调用脚本:
sh script-name
这时shell解释程序“sh”作为一个外部命令被显式地调用,而script-name作为该命令的命令行参数可以被我们ps到。
另外,如果你的系统上有pidof命令可用,它倒是可以找出shell脚本进程(实际上应该是执行shell脚本的子shell进程)的进程ID:
pidof -x script-name

Q3: shell何时在子shell中执行命令?
子shell的话会保留之前的变量,子进程只会保留export的变量
A: 在此我们主要讨论Bourne shell及其兼容shell。在许多情况下shell会在子shell中执行命令:
1.(...)结构
小括号内的命令会在一个子shell环境中执行,命令执行的结果不会影响当前的shell环境。需要注意是此时变量$$会显示当前shell的进程id,而不是子shell的进程id。
参考:
{...;}结构中的命令在当前shell中执行,(内部)命令执行的结果会影响当前的shell环境。
2.后台执行或异步执行
command&
命令由一个子shell在后台执行,当前shell立即取得控制等候用户输入。后台命令和当前shell的执行是并行的,但没有互相的依赖、等待关系,所以是异步的并行。
3.命令替换
`command`(Bourn shell及兼容shell/csh)
$(command)(在ksh/bash/zsh中可用)
将command命令执行的标准输出代换到当前的命令行。command在子shell环境中执行,结果不会影响当前的shell环境。
4.管道(不同的shell处理不同)
cmd1|cmd2
cmd1和cmd2并行执行,并且相互有依赖关系,cmd2的标准输入来自cmd1的标准输出,二者是“同步”的。
对管道的处理不同的shell实现的方式不同。
在linux环境下大多数shell(bash/pdksh/ash/dash等,除了zshell例外)都将管道中所有的命令在子shell环境中执行,命令执行的结果不会影响当前的shell环境。
Korn shell的较新的版本(ksh93以后)比较特殊,管道最后一级的命令是在当前shell执行的。这是一个feature而非BUG,在POSIX标准中也是允许的。这样就使下面的命令结构成为可能:
command|read var
由于read var(read是一个内部命令)在当前shell中执行,var的值在当前shell就是可用的。
反之bash/pdksh/ash/dash中read var在子shell环境中执行,var读到的值无法传递到当前shell,所以变量var无法取得期望的值。类似这样的问题在各种论坛和news group中经常被问到。个人认为command|read var的结构很清晰,并且合乎逻辑,所以我认为Korn shell的这个feature很不错。可惜不是所有的shell都是这样实现的。:(如开源的pdksh就是在子shell执行管道的每一级命令。
Korn shell对管道的处理还有一个特殊的地方,就是管道如果在后台执行的话,管道前面的命令会由最后一级的命令派生,而不是由当前shell派生出来。据说Bourne shell也有这个特点(标准的Bourne shell没有测试环境,感兴趣的朋友有条件的可以自行验证)。但是他们的开源模仿者,pdksh和ash却不是这样处理。
最特殊的是zshell,比较新的zshell实现(好像至少3.0.5以上)会在当前shell中执行管道中的每一级命令,不仅仅是最后一条。每一条命令都由当前shell派生,在后台执行时也是一样。可见在子sehll中执行管道命令并不是不得已的做法,大概只是因为实现上比较方便或者这样的处理已经成为unix的传统之一了吧。;-)
让我们总结一下,不同的shell对管道命令的处理可能不同。有的shell中command|read var这样的结构是ok的,但我们的代码出于兼容性的缘故不能依赖这一点,最好能避免类似的代码。
5.进程替换(仅bash/zsh中,非POSIX兼容)
<(...)
>(...)
与管道有点类似,例子:cmd1 <(cmd2) >(cmd3), cmd1, cmd2, cmd3的执行是同步并行的。
<(command)形式可以用在任何命令行中需要填写输入文件名的地方,command的标准输出会被该命令当作一个输入文件读入。
>(command)形式可以用在任何命令行中需要填写输出文件的地方,该命令的输出会被command作为标准输入读入。
两种形式中的command都在子shell环境中执行,结果不会影响当前的shell环境。
6.if或while命令块的输入输出重定向
在SVR4.2的Bourne shell中对此情况会fork一个子shell执行if块和while块中的命令;在linux下似乎其它的shell中都不这样处理。
7.协进程(ksh)
只有Korn shell和pdksh有协进程的机制(其它shell中可以用命名管道来模拟)。类似于普通的后台命令,协进程在后台同步运行,所以必须在子shell中运行。协进程与后台命令不同的是它要和前台进程(使用read -p和print -p)进行交互,而后者一般只是简单地异步运行。

Q4: 既然在当前shell中执行命令也会派生子shell,那么它与在子shell中执行命令又有什么区别呢?
A: 这种说法不准确。
在当前shell中执行内部命令不会派生子shell,因此有些内部命令才能够改变当前的shell执行环境。
在当前shell中执行外部命令或脚本时会派生子shell,所以这时命令的执行不会影响当前 的shell环境。注意:子shell中执行的内部命令只会改变子shell的执行环境,而不会改变当前shell(父shell)的环境。

Q5: 怎样把子shell中的变量传回父shell?
A: 例如(echo "$a") | read b不能工作,如何找到一个替代方案?下面给出一些可能的方案:
1.使用临时文件
...
#in subshell
a=100
echo "$a">tmpfile
...
#in parent
read b<tmpfile
2.使用命名管道
mkfifo pipef
(...
echo "$a" > pipef
...)
read b <pipef
3.使用coprocess(ksh)
( echo "$a" |&)
read -p b
4.使用命令替换
b=`echo "$a"`
5.使用eval命令
eval `echo "b=$a"`
6.使用here document
read b <<END
`echo "$a"`
END
7.使用here string(bash/pdksh)
read b <<<`echo "$a"`
8.不用子shell,用.命令或source命令执行脚本。
即在当前shell环境下执行脚本,没有子shell,也就没有了子shell的烦恼。:)
14:55:31#tp#~> cat a.sh
echo $BASH_SUBSHELL
14:55:51#tp#~> . a.sh
0
source没建立子shell
解决的方法还不止于此,其它的进程间通信手段应该也能使用,这有待于大家一起发掘了。

 

你可能感兴趣的:(subShell与代码块)