bash 中的子 shell 可以分为以下两类:
subshell:通过进程替换、(LIST)、|或&创建的子shell。因为这种子shell在创建时只使用了fork函数,导致其可以继承父shell的全部变量(局部变量也会被继承,甚至包括`bash中的子shell可以分为以下两类:和$PID)、别名和函数。child shell:我们调用bash命令生成交互式shell或执行脚本都会得到这种子shell。这种子shell在创建时会通过fork和exec函数将其代码、数据、堆栈替换为全新的bash进程,因此只会继承父shell传递来的全局变量。
shell启动的shell子进程称为子shell。直接以文件名运行可执行文件时,bash并不知道它调用的一个可执行是二进制文件还是脚本,只是在exec过程中交给系统内核处理。对于shell脚本,通常以“#![shell可执行文件名]”开头,“#!”是一种magic number。当内核通过magic number断定执行的是脚本时,就会调用一个新的指定的shell的实例来解释执行脚本,这个实例就是子shell。父子shell是两个进程,所以各自的变量是独立的。除非父shell将自己的变量导出到环境中,否则子shell无法获得父shell中定义的变量。
bash通过变量SHLVL记录自己是进程调用栈中哪一层的shell,即bash被嵌套的深度。bash启动时,调用variables.c中的initialize_shell_level()设置SHLVL。系统login之后启动的bash的SHLVL为1,每层shell启动的子shell的SHLVL在其环境中读到的SHLVL基础上加1。
使用source命令(“.”命令)执行脚本时,不开启子shell。bash内部的实现是将脚本文件内容读入一个缓冲区,然后执行语法分析,因此效果与直接从键盘输入脚本内容相同。
创建 subshell
# 创建 subshell 的方法
> echo `echo $BASH_SUBSHELL`
1 # 命令替换会创建 subshell
> echo $BASH_SUBSHELL &
1 # 后台执行会创建 subshell
> (echo $BASH_SUBSHELL)
1 # 一对小括号会创建 subshell
> function f1()
> {
> echo $BASH_SUBSHELL >&2
> }
> f1
0
> f1 | f1 | f1
1 # 三个独立的 subshell
1
1
# 管道陷阱
> a=10
> a=5 | echo $a
10 # 左右两侧都在各自创建的 subshell 中执行 互不影响
> echo $a
10 # 在 subshell 中修改变量不会影响父 shell
# 证明确实创建了子 shell
> pstree -p $; echo $ $SHLVL $BASH_SUBSHELL;
bash(2695)───pstree(2702)
2695 1 0
> (pstree -p $; echo $ $SHLVL $BASH_SUBSHELL;)
bash(2695)───bash(2714)───pstree(2715)
2695 1 1 # $也被继承了,但注意其值是 invorking shell 的 PID
# LIST 必须为多条命令才会创建子 shell
> pidof bash; (pidof bash;)
2695
2695
> pidof bash; (echo>/dev/null;pidof bash;)
2695
3163 2695
# 只有 subshell 可以继承局部变量
> unset a; a=1
> (echo "a is $a in the subshell")
a is 1 in the subshell # 仅 fork 完全拷贝
> bash -c 'echo "a is $a in the child shell"'
a is in the child shell # fork 并 exec 导致信息被覆盖
# 查看嵌套层级
> (echo $SHLVL $BASH_SUBSHELL;(echo $SHLVL $BASH_SUBSHELL;(echo $SHLVL $BASH_SUBSHELL)))
1 1
1 2
1 3
> bash
> (echo $SHLVL $BASH_SUBSHELL;(echo $SHLVL $BASH_SUBSHELL;(echo $SHLVL $BASH_SUBSHELL)))
2 1
2 2
2 3
SHLVL 变量用于记录 shell 的嵌套层级, BASH_SUBSHELL 用于记录 subshell 的嵌套层级。
执行脚本
使用 bash 命令(相当于在当前 shell 中执行了外部命令)执行 shell 脚本时会创建 child shell ;使用 source 、 . 或 exec 则会直接在当前 shell 中执行脚本,不会额外创建 child shell (不包括脚本中的命令可能会自行创建的子 shell )。使用 bash 命令得到的 child shell 每次从脚本读取一行,每行命令都像直接来自键盘一样被读取、解释和执行,父 shell 在此过程中将通过 wait 系统调用等待其子进程完成。
bash my.sh # 会创建子 shell
./my.sh # 会创建子 shell
. my.sh # 不会创建子 shell,因为是内置命令
source my.sh # 不会创建子 shell,因为 `source` 是 `.` 的别称
简单总结一下子进程和子 shell :
- 执行内置命令和
shell脚本中的函数时,连子进程都不会创建,自然也不会创建子shell。
> mkfifo a
> fc1(){ a<a; } # 定义一个 shell 脚本函数
> echo a<a &
> pstree -pa $
bash,6080
├─bash,11092 # 这个 subshell 是 & 创建的
└─pstree,11095 -pa 6080
# 内置命令 echo 直接在 11092 中执行,甚至都没有建立子进程
# 可以另开一个终端然后 pstree -pa <上面那个bash的pid> 进行验证
> fc1 &
> pstree -pa 6080 # 在另一个终端中输入
bash,6080 # 只显示这个 没有创建任何子进程
- 执行外部命令时会使用
fork-exec模式创建子进程,如果外部命令是shell类型则会创建child shell。 - 使用
source、.、exec执行脚本以及用{ LIST }(注意是大括号)包裹命令列表相当于在当前shell中逐行输入命令,要注意使用exec执行完脚本后会退出shell。 - 使用
bash xxx.sh或./xxx.sh运行脚本会创建child shell。
> cat test.sh
#!/bin/bash
sleep 30;
> bash test.sh # 回车之后 ctrl+z
> pstree -pa $
bash,6080
├─bash,12602 test.sh # 自动创建的 child shell
│ └─sleep,12603 30 # 逐行解释执行的命令
└─pstree,12606 -pa 6080
- 使用
( LIST )执行命令列表会自动创建subshell。
> (echo;sleep 30;) # 回车之后按下 ctrl + z
> pstree -pa $
bash,6080 # 敲命令的 invoking shell
├─bash,9780 # 自动创建的 subshell,可与父 shell 共享局部变量
│ └─sleep,9781 30 # 执行外部命令创建的 child process
└─pstree,9782 -pa 6080
> fg 1 # 可以等待完成 也可以直接 ctrl+c