实例解析shell子进程(subshell)


实例解析shell子进程(subshell)
 
通过实例,解析个人对shell子进程的一个了解,主要包括以下几个方面
1:什么是shell子进程
2:shell什么情况下会产生子进程
3:子进程的特点与注意事项
4:$变量$$在脚本里的意义,及如何得到子进程里的进程号
 
参考文档:apue,bash的man和info文档
 
1:什么是shell子进程
 
子进程,是从父子进程的概念出发的,unix操作系统的进程从init进程开始(init进程为1,而进程号0为系统原始进程,以下讨论的进程原则上不包括进程0)均有其对应的子进程,就算是由于父进程先行结束导致的孤儿进程,也会被init领养,使其父进程ID为1。
 
也因为所有的进程均有父进程,事实上,所有进程的创建,都可视为子进程创建过程。在apue一书里提及unix操作系统进程的创建,大抵上的模式都是进行fork+exec类系统调用。
 
理解子进程的创建执行,需要至少细分到二个步骤,包括
通过fork创建子进程环境,
通过exec加载并执行进程代码。
其间诸如继承的环境变量等细节,可以查看apue第八章相关章节。
 
而shell子进程(以下均称subshell),顾名思义,就是由“当前shell进程”创建的一个子进程
 
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 subshell
3:括号命令列表
()操作符
     Placing a list of commands between parentheses causes a subshell
     environment to be created
4:执行外部脚本、程序:
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子进程的创建在步骤之一并无多大差别,一般还是父进程调用fork产生进程环境,估在第二步exec的时候,是存在差别的。
 
shell做为解释语言程序,提供给第二步exec加载和执行的程序体并不是脚本本身,而是由第一行#!指定的,默认为shell程序,当然也可以是awk,sed等程序,在之前写过的一篇文章里:shell脚本的set id如何生效就有提及。这里不再展开讨论。
 
只不过子进程的执行会根据情况而有所差别,对于内置函数,exec程序体为shell程序,并在会在子shell直接调用内置函数,
 
而外部函数或程序,在创建了子进程环境后,大致会有二种执行情况:
1:直接exec外部程序,
比如下边例子中直接执行的sleep,pstree命令等
2:subshellexec程序体为shell程序,在此基础上会进一步创建一个子进程以执行函数。
比如下边例子中通过函数提交后台程序中的shell命令等
 
 
例:内置函数(直接在subshell里执行,不管是否通过函数)
[root@localhost shell]# mkfifo a
[root@localhost shell]# type echo
echo is a shell builtin
[root@localhost shell]# b(){ echo a>a; }
[root@localhost shell]# b &
[1] 15697
[root@localhost shell]# echo a>a &
[2] 15732
[root@localhost shell]# pstree -pa $$
bash,571
  |-bash,15697
  |-bash,15732
  `-pstree,15734 -pa 571
 
例:定义函数并提交后台进行
(函数调用中的sleep在subshell之下又创建一个子进程,
而pstree,sleep命令的直接执行,则是直接在子进程上进行)
 
[root@localhost shell]#  a(){ sleep 30;  } ;
[root@localhost shell]# sleep 40 &
[1] 15649
[root@localhost shell]# a &
[2] 15650
[root@localhost shell]# pstree -pa $$
bash,571
  |-bash,15650
  |   `-sleep,15651 30
  |-pstree,15652 -pa 571
  `-sleep,15649 40
 
 
对于第四点,要注意,shell脚本的执行模式,在第四点的二种模式下,shell是会创建子进程的:
  filename ARGUMENTS
bash filename ARGUMENTS
 
但shell同时提供二种不创建子程序的进程创建方式
1:source命令,使用方法
Source   filename ARGUMENTS
.  filename ARGUMENTS
 
此种方法,直接在当前shell进程中执行filename脚本,filename结束后继续返回当前shell进程
 
2:exec命令,使用方法
Exec  filename ARGUMENTS
此种方法直接在当前shell进程中执行filname脚本,filename结束后退出当前shell进程
 
3:子进程的特点与注意事项
这方面不具体展开,只提一点写脚本容易出现的错误。
 
做为子进程,其进程环境与父进程的环境是独立的, 所以在变量传递过程中,需要注意子进程内部不能更改到父进程的变量。
 
比如如下通过管道求和并赋给外部变量sum例子,结果sum值并不会因此改变:
[root@localhost shell]# sum=0
[root@localhost shell]# echo '1 2 3 4' |sed 's/ //n/g'|while read line; do  sum+=$line; done
[root@localhost shell]# echo $sum
0
[root@localhost shell]#
 
 
4:变量$$在脚本里的意义
 
变量$$代表的是当前shell进程的进和id,这里要特别留意“当前shell”,
看看info bash里的说明
`$'
     Expands to the process ID of the shell.  In a `()' subshell, it
     expands to the process ID of the invoking shell, not the subshell.
再看看man bash里的说明
  $     
Expands  to  the process ID of the shell.  In a () subshell, it expands to the process ID of the current  shell, not the subshell.
 
 
所以在实际环境中,$$并不一定“当前进程”的进程号,而是当前shell进程的进程号。
从文档中,需要留意的便是 invoking shell (info) 或  current  shell(man) 与 当前subshell进程的关系了
 
这就引出了几个问题
1:到底怎么样算是  current  shell
2:子进程里的$$对应的是哪个  current  shell
3:如何猎取子进程的$$?
 
做为调试和测试,下边的例子引用几个变量,
 
BASH_SOURCE'
     An array variable whose members are the source filenames
     corresponding to the elements in the `FUNCNAME' array variable.
`BASH_LINENO'
     An array variable whose members are the line numbers in source
     files corresponding to each member of FUNCNAME.
     `${BASH_LINENO[$i]}' is the line number in the source file where
     `${FUNCNAME[$i]}' was called.  The corresponding source file name
     is `${BASH_SOURCE[$i]}'.  Use `LINENO' to obtain the current line
     number.
`FUNCNAME'
     An array variable containing the names of all shell functions
     currently in the execution call stack.  The element with index 0
     is the name of any currently-executing shell function.  The
     bottom-most element is "main".  This variable exists only when a
     shell function is executing.  Assignments to `FUNCNAME' have no
     effect and return an error status.  If `FUNCNAME' is unset, it
     loses its special properties, even if it is subsequently reset.
 
脚本里set -x,并设置PS4跟踪程序执行过程
 
PS4='+[$SHELL][$BASH_SUBSHELL][$PPID-$$][$LINENO]["${BASH_SOURCE[*]}"][${FUNCNAME[*]}][${BASH_LINENO[*]}]/n   +
 
 
PS4设置显示值如下:
[$SHELL]:当前shell路径
[$BASH_SUBSHELL]:子shell路径长度
[$PPID-$$]:父进程id,和变量$$值(current  shell进程ID)
[$LINENO]:在当前shell的命令行号
["${BASH_SOURCE[*]}"]:源脚本程序文件记录队列
[${FUNCNAME[*]}]:函数调用记录队列
[${BASH_LINENO[*]}]:执行行号记录队列
 
 
 
程序如下:
[root@localhost shell]# cat -n subshell.sh
+[/bin/bash][0][569-571][1060][""][][]
   +cat -n subshell.sh
     1  #!/bin/bash
     2
     3  set -x
     4  sub2() {
     5  #       sh subshell2.sh
     6          sleep 1
     7  }
     8  sub() {
     9          sub2 &
    10          sleep 20
    11  }
    12  sub  &
    13  pstree -p $PPID
 
 
执行结果如下:
 
[root@localhost shell]# bash subshell.sh
+[/bin/bash][0][569-571][1059][""][][]
   +bash subshell.sh
+[/bin/bash][0][571-17858][12]["subshell.sh"][][0]
   +sub
+[/bin/bash][0][571-17858][13]["subshell.sh"][][0]
   +pstree -p 571
+[/bin/bash][1][571-17858][10]["subshell.sh subshell.sh"][sub main][12 0]
   +sleep 20
+[/bin/bash][1][571-17858][9]["subshell.sh subshell.sh"][sub main][12 0]
   +sub2
+[/bin/bash][2][571-17858][6]["subshell.sh subshell.sh subshell.sh"][sub2 sub main][9 12 0]
   +sleep 1
bash(571)---bash(17858)-+-bash(17859)-+-bash(17860)---sleep(17863)
                        |             `-sleep(17862)
                        `-pstree(17861)
 
 
说明:
1:
首先在当前shell(进程id 571)下执行subshell.sh ,产生子进程,
[$PPID-$$]=[571-17858]显示此时执行subshell.sh脚本的进程号为17858,
[][0]显示未进行函数调用,未产生函数记录记录
说明在subshell.sh执行进程里,$$值保存的“current shell”即为本身,17858
 
[$LINENO]=[12]显示在subshell.sh第12行调用sub函数
sub函数在程序里通过&提交后台方式调用,
进程树显示,sub函数的调用在17858进程后创建子进程17859,执行体为bash
此时,ppid指示父进程为571,$$变量值为17858,
说明对于sub调用产生的进程,其“current shell”仍然为subshell.sh脚本执行进程17858
 
[$LINENO]=[13]显示在subshell.sh第13行执行pstree命令
pstree命令调用方式是在脚本里直接调用
进程树显示,pstree命令直接在17858进程后创建子进程17861并执行
此时,ppid指示父进程为571,$$变量值为17858,
说明对于这里运行的pstree命令的子进程,其“current shell”仍然为subshell.sh脚本执行进程17858
 
 
2:
[sub main][12 0]显示进入sub函数内部
 
[$LINENO]=[9]显示执行(在sub函数内)脚本第9行,调用sub2函数
进程树显示,sub2函数的调用在17859进程后创建子进程17860,执行体为bash
此时,ppid仍然指示父进程为571,$$变量值为17858,
说明对于sub2调用产生的进程,其“current shell”仍然为subshell.sh脚本执行进程17858
 
 
[$LINENO]=[10]显示执行(在sub函数内)脚本第10行,sleep命令
此处sleep命令调用方式是在脚本里的sub函数内直接调用
进程树显示,sleep命令是sub函数调用时创建的进程17859后创建子进程17862并执行
此时,ppid指示父进程为571,$$变量值为17858,
说明对于这里运行的pstree命令的子进程,其“current shell”仍然为subshell.sh脚本执行进程17858
 
3:
[sub2 sub main][9 12 0]显示进入sub2函数内部
[6]显示执行(在sub2函数内)脚本第6行,sleep 1
此处sleep命令调用方式是在脚本里的sub2函数内直接调用
进程树显示,sleep命令是sub2函数调用时创建的进程17860后创建子进程17863并执行
此时,ppid指示父进程为571,$$变量值为17858,
说明对于这里运行的sleep命令的子进程,其“current shell”仍然为subshell.sh脚本执行进程17858
 
终上
这里的$$只有二个值,
一个是最初的bash shell: 571,
一个是subshell.sh脚本调用时产生的进程:17858
其他由subshell.sh产生的子进程,无论是函数还是命令运行,$$变量值保存的“current shell”均为subshell.sh调用时产生的进程:17858
 
由此推论出上边提到的四种子shell的创建方法:提交后台,管道,括号命令列表,脚本调用。似乎只有第四种方法--脚本调用--产生的subshell可以做o为“current shell”
 
可以通过以下二个例子再次论证这个推论
例子一:
更改脚本调用方式,
此种方式采用当前shell进程执行subshell.sh,不再创建一个子进程
结果$$变量值保存的“current shell”均为当前进程
 
[root@localhost shell]# source  subshell.sh
+[/bin/bash][0][569-571][1062][""][][]
   +source subshell.sh
++[/bin/bash][0][569-571][3]["subshell.sh"][][1062]
   +set -x
++[/bin/bash][0][569-571][13]["subshell.sh"][][1062]
   +pstree -p 569
++[/bin/bash][0][569-571][12]["subshell.sh"][][1062]
   +sub
++[/bin/bash][1][569-571][1]["subshell.sh subshell.sh"][sub source][12 1062]
   +sub2
++[/bin/bash][2][569-571][2]["subshell.sh subshell.sh subshell.sh"][sub2 sub source][1 12 1062]
  sleep 1
++[/bin/bash][1][569-571][2]["subshell.sh subshell.sh"][sub source][12 1062]
   +sleep 20
sshd(569)---bash(571)-+-bash(18801)---sleep(18804)
                      |-bash(18806)---bash(18808)---sleep(18809)
                      `-pstree(18807)
 
 
例子二:
在子进程里边,采用脚本调用方式,或取子进程进程号
为此增加一个脚本subshell2.sh,并在subshell.sh进行调用
   +cat -n subshell2.sh
     1  #!/bin/bash
     2  set -x
     3  echo $PPID
     4  sleep 10
 
   +cat -n subshell.sh
     1  #!/bin/bash
     2
     3  set -x
     4  sub2() {
     5          ./subshell2.sh &
     6          sleep 1
     7  }
     8  sub() {
     9          sub2 &
    10          sleep 20
    11  }
    12  sub  &
    13  pstree -pa $PPID
 
 
 
[root@localhost shell]# ./subshell.sh
+[/bin/bash][0][569-571][1138][""][][]
   +./subshell.sh
+[/bin/bash][0][571-19715][13]["./subshell.sh"][][0]
   +pstree -pa 571
+[/bin/bash][0][571-19715][12]["./subshell.sh"][][0]
   +sub
+[/bin/bash][1][571-19715][9]["./subshell.sh ./subshell.sh"][sub main][12 0]
   +sub2
+[/bin/bash][2][571-19715][5]["./subshell.sh ./subshell.sh ./subshell.sh"][sub2 sub main][9 12 0]
 ./subshell2.sh
+[/bin/bash][2][571-19715][6]["./subshell.sh ./subshell.sh ./subshell.sh"][sub2 sub main][9 12 0]
 sleep 1
+[/bin/bash][1][571-19715][10]["./subshell.sh ./subshell.sh"][sub main][12 0]
   +sleep 20
+[/bin/bash][0][19718-19719][3]["./subshell2.sh"][][0]
   +echo 19718
19718
+[/bin/bash][0][19718-19719][4]["./subshell2.sh"][][0]
   +sleep 10
bash,571
  `-subshell.sh,19715 ./subshell.sh
      |-pstree,19717 -pa 571
      `-subshell.sh,19716 ./subshell.sh
          |-sleep,19721 20
          `-subshell.sh,19718 ./subshell.sh
              |-sleep,19720 1
              `-subshell2.sh,19719 ./subshell2.sh
                  `-sleep,19722 10
 
从上边的执行结果可以看出,当程序执行进入subshell2.sh时,创建了进程号19294的进程境,$$变量值保存的“current shell”均为更新为subshell2.sh调用时创建的进程:
 
这样,打出来的subshell2.sh的父进程id,实际上就是sub2调用时创建的进程,这样就把一些$$本来显示不出来的子进程id给显示了出来。
 

相关内容

    暂无相关文章