Bash 子 shell

警告
本文最后更新于 2023-10-22,文中内容可能已过时。

Bash 子 shell

什么是子 shell

子 shell 是指在操作系统中创建的一个新的 shell 进程,它是由父 shell(父进程)派生出来的。子 shell 可以独立运行,但它仍然受到父 shell 的控制和影响。子 shell 可以执行与父 shell 不同的命令和操作,但在子 shell 中所做的更改不会影响到父 shell 或其他子 shell。

子 shell 通常用于在父 shell 的环境中执行一系列命令,而不会影响到父 shell 的状态。这对于需要进行一些临时性的操作、测试或脚本执行非常有用。当子 shell 执行完毕后,控制权会返回到父 shell。

子 shell 和父 shell 的关系其实就是子进程和父进程的关系,子 shell 会从父 shell 中继承很多环境,如变量、命令全路径、文件描述符、当前工作目录、陷阱等等,但子 shell 有很多种类型,不同类型的子 shell 继承的环境不相同。

可以使用$BASH_SUBSHELL变量来查看从当前进程开始的子 shell 层数,$BASHPID查看当前所处 BASH 的 PID,这不同于特殊变量$$,因为$$在大多数情况下都会从父 shell 中继承。

$$变量保存的是当前 shell 进程 ID 号

如何产生子 shell?

参考博客:子 shell 以及什么时候进入子 shell - 骏马金龙 - 博客园

首先 shell 进程本质上是 Linux 进程,Linux 创建子进程的方式有三种:

  • fork 出来的进程,它会复制当前进程的副本 (不考虑写时复制的模式),以适当的方式将这些资源交给子进程。所以子进程掌握的资源和父进程是一样的,包括内存中的内容,所以也包括环境变量和变量。但父子进程是完全独立的,它们是一个程序的两个实例。

  • exec 出来的进程,exec 是加载另一个应用程序,替代当前运行的进程,也就是说在不创建新进程的情况下加载一个新程序。exec 还有一个动作:在进程执行完毕后,退出 exec 所在的 shell 环境。

  • clone 出来的进程。此处无需关心 clone,因为它用来实现 Linux 中的线程。

为了保证进程安全,若要形成新的且独立的子进程,都会先 fork 一份当前进程,然后在 fork 出来的子进程上调用 exec 来加载新程序替代该子进程。例如在 bash 下执行 cp 命令,会先 fork 出一个 bash,然后再 exec 加载 cp 程序覆盖子 bash 进程变成 cp 进程。

何时产生子 shell?

那什么情况下会进入子 shell 环境,什么时候不进入子 shel 环境呢?

判断是否进入了子 shell 的方式非常简单,执行echo $BASHPID,如果该值和父 bash 进程的 pid 值不同,则表示进入了子 shell。

  • 执行 bash 内置命令时,父进程不会创建子进程来执行这些命令。但如果将内置命令放在管道后,则此内置命令将和管道左边的进程同属于一个进程组,所以仍然会创建子 shell

    通过type命令查看命令的类型,返回builtin就是内置命令,比如type echo返回echo is a shell builtin,具体请看《Bash 的基本语法》

  • 执行bash命令本身时。事实上 fork 出来的 bash 子进程内容完全继承父 shell,但因重新加载了环境配置项,所以子 shell 没有继承普通变量。在执行bash命令后从变量$BASH_SUBSHELL的值为 0 可以认为是进入了一个完全独立的、全新的 shell 环境,而不应该认为是进入了片面的子 shell 环境

    关于bash命令加载的环境变量,请看《Bash 启动时的配置文件加载》

  • 执行 shell 脚本时。执行脚本的方式可以是bash test.sh,也可以是直接./test.sh(这要求脚本中第一行是#!/bin/bash),这两种方式和上面的执行bash进入子 shell 其实是一回事,都是使用bash命令进入子 shell。只不过此时的bash命令和直接执行bash命令所隐含的选项不一样(主要是交互式和非交互式的区别),所以继承和加载的 shell 环境也不一样。事实也确实如此,它仅只继承父 shell 的某些环境变量,其余环境一概初始化。另外,执行 shell 脚本相比于直接执行bash命令,还多了一个动作:脚本执行完毕后自动退出子 shell。

    具体请看《Bash 启动时的配置文件加载》

  • 执行 shell 函数时。其实 shell 函数就是命令,它和 bash 内置命令的情况一样。直接执行时不会进入子 shell,但放在管道后会进入子 shell。

  • 执行非 bash 内置命令时。例如执行cp命令、grep命令等,会进入完全的新的 shell 环境,

  • 子命令拓展时,比如echo $(ls),将开启一个子 shell 先执行子命令,再将执行结果返回给当前命令。因为这次的子 shell 不是通过bash命令进入的子 shell,所以它会继承父 shell 的所有变量内容。

  • 使用括号()组合一系列命令时。例如(ls;date;echo haha),独立的括号将会开启一个子 shell 来执行括号内的命令。这种情况等同于执行非 bash 内置命令时。

  • 放入后台运行的任务,一般通过在语句末尾加上&来将语句放到后台执行,它不仅是一个独立的子进程,还是在子 shell 环境中运行的。例如echo hahha &

  • 进程替换。既然是新进程了,当然进入子 shell 执行。例如cat <(echo haha)

再说明$$的继承问题。除了直接执行 bash 命令和 shell 脚本这两种子 shell,其他进入子 shell 的情况都会继承父 shell 的值。前面也已经说了,其实 shell 脚本和直接执行 bash 命令开启子 shell 的方式是一样的,它们都不会继承"$$“值,可以根据上述实验自行测试。

需要说明的是,子 shell 的环境设置不会粘滞到父 shell 环境,无论是使用 export 还是 source ,都只能是父 shell 同步到子 shell,不是也不会是从子 shell 同步到父 shell。也就是说子 shell 的变量等不会影响父 shell。

0%