上节我们说了子 Shell 和子进程的区别,这节就来看一下如何检测它们。
我们都知道使用 $ 变量可以获取当前进程的 ID,我在父 Shell 和子 Shell 中都输出 $ 的值,只要它们不一样,不就是创建了一个新的进程吗?那我们就来试一下吧。
[mozhiyan@localhost ~]$ echo $$ #父Shell PID
3299
[mozhiyan@localhost ~]$ (echo $$) #组命令形式的子Shell PID
3299
[mozhiyan@localhost ~]$ echo "http://c.biancheng.net" | { echo $$; } #管道形式的子Shell PID
3299
[mozhiyan@localhost ~]$ read <
[mozhiyan@localhost ~]$ echo $REPLY
3299
你看,子 Shell 和父 Shell 的 ID 都是一样的,哪有产生新进程了?作者你是不是骗人呢?
其实不是我骗人,而是你掉坑里了,因为 $ 变量在子 Shell 中无效!Base 官方文档说,在普通的子进程中,$ 确实被展开为子进程的 ID;但是在子 Shell 中,$ 却被展开成父进程的 ID。
除了 $,Bash 还提供了另外两个环境变量——SHLVL 和 BASH_SUBSHELL,用它们来检测子 Shell 非常方便。
SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,每次进入一层普通的子进程,SHLVL 的值就加 1。而 BASH_SUBSHELL 是记录一个 Bash 进程实例中多个子 Shell(sub shell)嵌套深度的累加器,每次进入一层子 Shell,BASH_SUBSHELL 的值就加 1。
1) 我们还是用实例来说话吧,先说 SHLVL。创建一个脚本文件,命名为 test.sh,内容如下:
#!/bin/bash
echo "$SHLVL $BASH_SUBSHELL"
然后打开 Shell 窗口,依次执行下面的命令:
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
2 0
[mozhiyan@localhost ~]$ bash #执行bash命令开启一个新的Shell会话
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
3 0
[mozhiyan@localhost ~]$ bash ./test.sh #通过bash命令运行脚本
4 0
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
3 0
[mozhiyan@localhost ~]$ chmod +x ./test.sh #给脚本增加执行权限
[mozhiyan@localhost ~]$ ./test.sh
4 0
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
3 0
[mozhiyan@localhost ~]$ exit #退出内层Shell
exit
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
2 0
SHLVL 和 BASH_SUBSHELL 的初始值都是 0,但是输出结果中 SHLVL 的值从 2 开始,我猜测 Bash 在初始化阶段可能创建了子进程,我们暂时不用理会它,将关注点放在值的变化上。
仔细观察的读者应该会发现,使用 bash 命令开启新的会话后,需要使用 exit 命令退出才能回到上一级 Shell 会话。
bash ./test.sh和chmod +x ./test.sh; ./test.sh这两种运行脚本的方式,在脚本运行期间会开启一个子进程,运行结束后立即退出子进程。
2) 再说一下 BASH_SUBSHELL,请看下面的命令:
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
2 0
[mozhiyan@localhost ~]$ (echo "$SHLVL $BASH_SUBSHELL") #组命令
2 1
[mozhiyan@localhost ~]$ echo "hello" | { echo "$SHLVL $BASH_SUBSHELL"; } #管道
2 1
[mozhiyan@localhost ~]$ var=$(echo "$SHLVL $BASH_SUBSHELL") #命令替换
[mozhiyan@localhost ~]$ echo $var
2 1
[mozhiyan@localhost ~]$ ( ( ( (echo "$SHLVL $BASH_SUBSHELL") ) ) ) #四层组命令
2 4
你看,组命令、管道、命令替换这几种写法都会进入子 Shell。
注意,“进程替换”看起来好像产生了一个子 Shell,其实只是玩了一个障眼法而已。进程替换只是借助文件在()内部和外部的命令之间传递数据,但是它并没有创建子 Shell;换句话说,()内部和外部的命令是在一个进程(也就是当前进程)中执行的。
我们不妨来实际检测一下:
[mozhiyan@localhost ~]$ echo "$SHLVL $BASH_SUBSHELL"
2 0
[mozhiyan@localhost ~]$ echo "hello" > >(echo "$SHLVL $BASH_SUBSHELL")
2 0
SHLVL 和 BASH_SUBSHELL 变量的值都没有发生改变,说明进程替换既没有进入子进程,也没有进入子 Shell。