免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
1234下一页
最近访问板块 发新帖
查看: 13966 | 回复: 36

[shell问答录]:命令、进程、子shell... [复制链接]

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
发表于 2006-04-19 18:14 |显示全部楼层
前些天在CU上讨论一个统计正在执行的脚本数量的问题过程中,发现自己对于shell如何执行命令方面了解还是甚少,惭愧惭愧...期间得到waker兄的指点,在此表示感谢!他的说法除了个别地方不太准确外,基本上是正确的。这些天抽时间找了些资料研究了一下,又学到了不少!这里把我的一点心得以问答的形式贴出来,供大家参考。小弟才疏学浅,错误的地方一定很多,欢迎大家拍砖、指正!

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中执行命令?
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的烦恼。

解决的方法还不止于此,其它的进程间通信手段应该也能使用,这有待于大家一起发掘了。^_^

————————————————————————————————————————————————————
参考帖子:
waker兄的帖子

原讨论贴

[ 本帖最后由 woodie 于 2006-4-19 18:20 编辑 ]

评分

参与人数 1可用积分 +4 收起 理由
r2007 + 4 精品文章

查看全部评分

论坛徽章:
0
发表于 2006-04-19 19:10 |显示全部楼层
好,总结的好

论坛徽章:
7
荣誉版主
日期:2011-11-23 16:44:17子鼠
日期:2014-07-24 15:38:07狮子座
日期:2014-07-24 11:00:54巨蟹座
日期:2014-07-21 19:03:10双子座
日期:2014-05-22 12:00:09卯兔
日期:2014-05-08 19:43:17卯兔
日期:2014-08-22 13:39:09
发表于 2006-04-19 20:23 |显示全部楼层
好文,先赞一个,有空好好研究一下

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
发表于 2006-04-20 08:04 |显示全部楼层
3.使用coprocess(ksh)
( echo "$a" |&)
read -p b

in  kornshell try
echo "$a"|read b
echo $b

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
发表于 2006-04-20 08:19 |显示全部楼层
Q1中

A: 这里的简单命令和bash参考手册里的含义相同,形式上一般是:命令的名称加上它的参数+赋值操作+IO重定向描述
从上面看
var=value不是一个简单命令,而是上面的第二部分

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
发表于 2006-04-20 08:23 |显示全部楼层
原帖由 waker 于 2006-4-20 08:04 发表
3.使用coprocess(ksh)
( echo "$a" |&)
read -p b

in  kornshell try
echo "$a"|read b
echo $b

正确!在Korn shell和zshell的较新版本这样是可以的,但注意pdksh不行。这一点Q3之4已经详细讨论过了。谢谢关注!

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
发表于 2006-04-20 10:10 |显示全部楼层
这也就是为什么ps不能找到正在运行的脚本的名字的原因了。
或许有另一种解释
如果找不到,不妨在脚本上加入 #!试试
这个可以从manpage中找到解释

       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.
       This  subshell reinitializes itself, so that the effect is as if a new
       shell had been invoked to handle the script, with the  exception  that
       the  locations  of  commands  remembered by the parent (see hash below
       under SHELL BUILTIN COMMANDS) are retained by the child.

       If the program is a file beginning with #!, the remainder of the first
       line specifies an interpreter for the program.  The shell executes the
       specified interpreter on operating systems that  do  not  handle  this
       executable  format  themselves.  The arguments to the interpreter con-
       sist of a single optional argument following the interpreter  name  on
       the  first  line  of the program, followed by the name of the program,
       followed by the command arguments, if any.

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
发表于 2006-04-20 11:21 |显示全部楼层
先谢谢版主加精鼓励!
原帖由 waker 于 2006-4-20 08:19 发表
Q1中

A: 这里的简单命令和bash参考手册里的含义相同,形式上一般是:命令的名称加上它的参数+赋值操作+IO重定向描述
从上面看
var=value不是一个简单命令,而是上面的第二部分


waker兄说的对!嗯,这里是说得不太严谨。
通常我们都会把单独的赋值操作“当作”一条命令,实际使用中也是这样。如:
for i in 1 2 3; do
  a$i=$i
done
赋值显然是作为一条命令来使用的。
但是深究起来,赋值到底是那种类型的命令呢?它显然不是外部命令或脚本命令,也不是函数或alias。那么它是内部命令吗?但在shell的内部命令列表中你又找不到它。
我想赋值系列符号:=, +=, -=,*/,/=, ...应该理解为shell的操作符,变量名和所赋的值是它的“操作对象”,这就可以解释在命令前面的赋值操作,如:
VAR=value command
此处的赋值与IO重定向操作一样,不是单独的命令。
但赋值操作又的确可以作为命令使用,这时,我宁愿把它理解成一条特殊的“内部命令”。
至于你提到的重定向操作,的确有的重定向操作是被作为“简单命令”的一部分来处理的;但是有的重定向操作符却不能这样,如“|”,它是被用来连接多个命令成为一个组合命令来使用的。
情况似乎越来越复杂了!OK,我们不妨从反面来界定一下,什么不是简单命令?
其一,循环/分支等用于流控的复杂命令(字面意义):
if cmd1; then
  cmd2
elif cmd3; then
  cmd4
fi
while true; do
  cmd1
  cmd2
  ...
done
等等,不再列举。
其二,命令列表,如:
cmd1;cmd2
cmd1&&cmd2||cmd3
cmd1&cmd2
...
其三,命令分组:
(cmd1;cmd2)
{ cmd1;cmd2;}
...
其四,管道,如:
cmd1|cmd2

其五,上面提到过的函数或alias,他们都可以组合多条命令因此排除在简单命令之外。
...
其六,命令替换或进程替换,它们也引入了别的命令。
...
也许还有。
...

其实我们使用“简单命令”这个概念只是为了简化问题和能有一个进一步讨论的共同基点,没有必要追究它的定义是否准确、完备。其实完全从字面来理解亦无不可,就是指最简单的“一条命令”,多条命令的组合不算。

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
发表于 2006-04-20 11:42 |显示全部楼层
原帖由 waker 于 2006-4-20 10:10 发表
这也就是为什么ps不能找到正在运行的脚本的名字的原因了。
或许有另一种解释
如果找不到,不妨在脚本上加入 #!试试
这个可以从manpage中找到解释

       If this execution fails because the file is not  ...

不加首行的#!的脚本如何能用脚本名自己执行呢?呵呵。^_^
如果首行的解释器与当前shell种类不同,确实可以ps到,不过ps到的脚本名仍是命令行参数而不是命令名。
如果解释器与当前shell是同一种shell,只能ps到当前shell的另一个副本而已。测试是在linux下做的,也许是shell内部的处理不同吧。
我的看法:无论如何,脚本不作为单独的进程运行,单独运行的是脚本的解释器进程。

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
发表于 2006-04-20 16:16 |显示全部楼层
A:不加首行的#!的脚本如何能用脚本名自己执行呢?呵呵。^_^

*:那一段不是有解释么?


B:至于你提到的重定向操作,的确有的重定向操作是被作为“简单命令”的一部分来处理的;但是有的重定向操作符却不能这样,如“|”,它是被用来连接多个命令成为一个组合命令来使用的。

*:恩,你自己已经找到答案了,加了|就不是simple command了,pipeline也不等于重定向哦

C:但赋值操作又的确可以作为命令使用,这时,我宁愿把它理解成一条特殊的“内部命令”。
*:如果command取 `command line'中的command,我想应该是没问题的,如果取`simple command'中的command,我认为是不恰当的,因为前者的command是不是翻译成“语句”比较合适? for do done if fi是不是一个“内部命令”呢?

以上是我对bash manpage中一些内容的理解

关于进程,可能我们的理解不同,我的理解打个比方,就象单独用车和石头来定义“运石头过程”都是不确切的一样,而且在这个过程中,车和石头都是可以换的,不能换的就是“本次过程的标志”,对系统来说就是PID
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP