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, 因此当管道结束后, 变量就消失了.
(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 解决的方法还不止于此,其它的进程间通信手段应该也能使用,这有待于大家一起发掘了。