如何检测子Shell和子进程

上节我们说了子 Shell 和子进程的区别,这节就来看一下如何检测它们。

我们都知道使用 $ 变量可以获取当前进程的 ID,我在父 Shell 和子 Shell 中都输出 $ 的值,只要它们不一样,不就是创建了一个新的进程吗?那我们就来试一下吧。
[mozhiyan@localhost ~]$ echo $$  #父Shell PID
3299
[mozhiyan@localhost ~]$ (echo $$)  #组命令形式的子Shell PID
3299
[mozhiyan@localhost ~]$ echo "https://www.xinbaoku.com" | { echo $$; }  #管道形式的子Shell PID
3299
[mozhiyan@localhost ~]$ read < <(echo $$)  #进程替换形式的子Shell PID
[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.shchmod +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。