ChildShell、SubShell、环境变量
推荐链接:
shell 及种类
相关链接: 壳层 、 Unix shell 、 C Shell 、 Bourne shell 、 dash 、 Bash 、 fish 、 bash到fish的命令翻译对照
用户默认交互式 shell
Bash,Unix shell 的一种,能运行于大多数 类Unix系统 的操作系统上,包括 Linux 与 Mac OS X v10.4 都将它作为默认 shell。Bash 是 Bourne shell 的后继兼容版本与开放源代码版本,它的名称来自 Bourne shell(sh)的一个双关语(Bourne again / born again):Bourne-Again SHell。
当用户登录到终端时,默认的 shell 程序就会运行。系统启动什么样的 shell 程序取决于个人的用户配置。如下所示,root 用户使用 bash shell
作为自己的默认 shell 程序。
Linux 用户和用户组管理 :包括对 /etc/passwd
文件等详细说明。
1 | $ cat /etc/passwd |
bash shell 程序位于 /bin
目录下,使用长列表可以看出它是一个可执行程序:
ls命令 -F:在每个输出项后追加文件的类型标识符,具体含义:“*”表示具有可执行权限的普通文件,“/”表示目录,“@”表示符号链接,“|”表示命令管道FIFO,“=”表示sockets套接字。当文件为普通文件时,不输出任何标识符;
1 | $ ls -lF /bin/bash |
像 /bin/tcsh
或 /bin/dash
或 /bin/csh
这些都是 shell 程序,由于 bash shell 使用者最多,很少有人使用其它 shell 作为默认 shell 。
默认系统 shell
Bourne shell 或 sh,它的二进制程序文件在大多数 Unix 系统上位于 /usr/bin/sh
或 /bin/sh
。在很多 Unix 版本中,它仍然是 root 的默认 shell。
在 Linux 系统中,会使用 软链接 将默认的系统 shell 设置成 bash 或 dash,那么 sh 其实就是 bash 或 dash 程序。
1 | $ ls -li /bin/sh |
使用长列表格式查看 软连接实际指向的文件位置 和 文件类型:
1 | $ ls -liF /bin/sh |
带有 ->
符号的就是 软连接 。
注意
有些发行版系统上,默认交互式 shell 和默认系统 shell 并不相同。
例如在 Ubuntu 发行版中(上文已列出),默认交互式 shell 是 bash,而默认系统 shell 是 dash。而且并不是必须一直使用默认的交互式 shell ,可以使用发行版中所有可用的 shell 。可以直接输入命令 /bin/dash
来启动 dash shell (子 shell 进程)。
父子 shell (ChildShell)
创建 ChildShell 进程
上面说的默认 bash shell ,是一个父 shell 进程 。要创建一个 ChildShell 进程很简单,输入 bash 就会创建一个新的 shell 进程,被称为 ChildShell 进程。要区分是父 shell 进程还是 ChildShell 进程,需要使用 ps
查看进程命令。在 root 用户下,我们先查看一下没有 ChildShell 进程的父 shell 进程是什么样子:
1 | $ ps -f |
接下来,输入 bash
,来创建一个 ChildShell 进程,该 ChildShell 进程的 PPID(父进程 ID)是 5895,对应 -bash 进程的 PID 5895。
1 | $ bash |
ChildShell 再创建 ChildShell
上面创建的 ChildShell 进程 PID 为 22335 。ChildShell 进程也可以再创建一个 ChildShell 进程,以此类推。它们的关系如下图所示:
下面继续建了两个 ChildShell 进程,总共三个 ChildShell 进程:
1 | $ bash |
退出 ChildShell 进程
通过输入 exit 命令能有条不紊的退出 ChildShell 进程:
1 | $ exit |
当没有了 ChildShell 时,再输入 exit 将退出控制台终端。
性能
生成 ChildShell 进程的成本不低,而且速度还慢,创建嵌套的 ChildShell 进程去处理命令,性能更为严重。
深度
1 | echo $SHLVL # ChildShell进程嵌套深度 |
父子 shell (SubShell)
进程列表 SubShell
在 shell 中,将命令放入 ( 括号 )
中,并在命令列表尾部加分号 ;
。这样便会创建出一个子进程去执行。是否生成了 SubShell,需要借助 echo $BASH_SUBSHELL
(此环境变量表示当前 SubShell 环境的嵌套级别),如果该命令返回 0
,表示没有 SubShell。一组命令放入括号中,产生了一个 SubShell 去执行
1 | $ echo $BASH_SUBSHELL # 在任何shell中输出结果都是0 |
上面说了,把命令列表放入括号中,就会创建一个 SubShell,下面在括号中再套一个括号, 结果是在 SubShell 上创建了 SubShell。
1 | $ (pwd; ls; (echo $BASH_SUBSHELL)) |
后台模式
在交互式 shell 中,一个高效的 SubShell 用法就是使用后台模式,在后台模式中运行命令在处理命令的同时让出 CLI ,以供他用。 演示后台模式经典命令就是 sleep
。想要将命令置入后台模式。在命令末尾加上字符 &
。 结合 sleep
暂停 10 秒,通过 ps
命令来看看。
将 sleep
命令置入后台模式,执行命令 sleep 10&
,出现了后台作业号 1,以及后台作业进程 29332。通过 ps
可以看到,该后台命令进程 29332 一直在运行。
1 | $ sleep 10& |
通过 jobs
命令可以查看当前后台运行的进程信息(另外 jobs -l 显示进程号),如下所示:
1 | $ sleep 10& |
进程列表 SubShell – 后台模式
上面讲到了进程列表 SubShell
,进程列表 是运行在 SubShell 中的一条或多条命令。
下面将进程列表置入后台模式,如下所示:当一级 SubShell 的数字 1 显示在提示符的旁边时,按下回车键。
1 | $ (sleep 2; echo $BASH_SUBSHELL)& |
在使用 shell 时,很多情况下,可以将命令置入后台模式,例如:下面将 mysql 的慢日志归档。
1 | $ pwd |
性能
在 shell 脚本中,经常会使用 SubShell 进行多进程处理,直接使用 SubShell 明显拖慢处理速度,因为多进程共用一个终端,这个终端控制着所有 SubShell 的 I/O 。解决办法:使用后台模式避免终端控制 I/O 。
将 SubShell 要处理的进程置入后台模式,即可以在 SubShell 中进行繁重的处理工作,又不会让 SubShell 的 I/O 受制于终端。
深度
1 | echo $BASH_SUBSHELL # SubShell进程嵌套深度 |
ChildShell 和 SubShell
ChildShell 和 SubShell 都拥有自己的进程,都是对应父进程的子进程。
shell 脚本创建的是 ChildShell 进程,测试脚本 test.sh:
1 | echo $BASH_SUBSHELL |
当我们创建一个 subshell 时,该 subshell 会有自己的进程,这个进程叫子进程(subshell 的进程)。
我们可以在 subshell 中创建很多子进程以完成作业,这些子进程的父进程就是这个 subshell 进程,这些子进程都是在这个 subshell 中完成作业(也许这样可以更好的控制环境变量吧!)。
以下内容摘自 SHLVL 和 BASH_SUBSHELL 两个变量的区别 :
SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,而 BASH_SUBSHELL 是记录一个 Bash 进程实例中多个子 Shell(subshell)嵌套深度的累加器。
如果你混淆了这两个变量,多半是对 BASH_SUBSHELL 这个变量名中的 subshell 概念不清,下面我们就讲讲什么是 subshell,什么不是。
很多人误以为在 Bash 里面再执行一次 Bash,或者再执行一个 Shell 脚本就是进入子 Shell 了,所以他们会有下面这样的疑问:
1 | bash # 执行另外一个 bash 命令 |
然而并不是,这些都不是子 Shell,这种情况只能描述成是“当前 Shell 启动了个外部命令,而这个外部命令刚好是个 Shell”,真正的子 Shell 是不需要重新执行硬盘上的外部命令的,全部是内存中的操作。上面这个示例中的 BASH_SUBSHELL 都应该替换成 SHLVL 才能看到累加效果。
有几本书给子 Shell 下过定义:
Advanced Bash-Scripting Guide 说:
A subshell is a child process launched by a shell (or shell script).
A subshell is a forked copy of the parent shell and shares it’s environment.
The Korn Shell: Unix & Linux Programming Manual 说:
A subshell is a separate copy of the parent shell, so variables, functions, and aliases from the parent shell are available to the subshell
第一本书流传较广,但它这句话说的太泛了,很容易误导人,虽然子 Shell 的确是当前 Shell 的子进程,但当前 Shell 的子进程不一定都是子 Shell(可能已经替换成了其他程序)。在 Bash 里面,只有特定的语法才会让代码进入子 Shell,比如管道两边的命令,比如用小括号括起来等等:
1 | (echo $BASH_SUBSHELL) |
真正的子 Shell 可以访问其父 Shell 的任何变量,而通过再执行一次 bash 命令所启动的 Shell 只能访问其父 Shell 传来的环境变量。这篇教程里面专门写了个例子:
For an example of the difference between a subshell and a child process that happens to be a shell:
1
2
3 unset a; a=1
(echo "a is $a in the subshell")
sh -c 'echo "a is $a in the child shell"'In the subshell, the regular shell variable
a
is visible; but because it is not exported, the full child process does not see it.
上面的例子中把当前 Shell 执行外部命令 sh 启动的 Shell 叫做 child shell,可惜在中文里还是得翻译成子 Shell。。。
从 c 语言层面讲,真正的子 Shell 是当前 Shell 进程调用了 fork() 函数,在内存中复制出一个几乎一模一样的子进程。而执行 bash 命令启动的所谓 child shell 是在执行 fork() 函数的基础上,又执行了一次 execve() 函数,execve() 函数会重新加载硬盘上的 bash 命令并执行,替换刚才 fork 出来的那个 shell 进程,除了传入的环境变量外,是个崭新的进程。
总结一下就是说,SHLVL 变量是记录了所谓的 child shell 的嵌套深度,而 BASH_SUBSHELL 是记录了 subshell 的嵌套深度。
把 child shell 叫成子 Shell,在口头上说说还可以,因为中文里没有其它什么好的叫法用来指代它,但你心里得明白,这不是术语子 Shell 真正的含义。
http://c.biancheng.net/view/3015.html
http://c.biancheng.net/view/3210.html
https://www.cnblogs.com/f-ck-need-u/p/7446194.html
外部命令、内建命令
Linux 命令有内部命令(内建命令)和外部命令之分,内部命令和外部命令功能基本相同,但也有些细微差别。
type命令 用来显示指定 命令类型,判断给出的指令是 内部指令 还是 外部指令。
内部命令
内部命令不需要使用子进程来执行,不需要借助外部程序文件来运行,它们已经和shell编译成一体,作为shell工具的组成部分存在。
它们是一些比较简单的 linux 系统命令,如 exit、history、cd、echo 等。
要分区是外部命令还是内部命令可以使用 type
来查看,如查看 cd
是内部还是外部命令:
1 | $ type cd |
alias命令
alias
命令也是一个内建命令,允许你为常用的命令和参数创建另一个名称,从而减少输入。在系统中已经设置好了一些常用的命令别名。查看当前可用的别名使用 alias -p
:
1 | $ alias -p |
下面通过 alias
命令来创建属于自己的别名。把 ls -li
长列表命令参数改别名为 li
。
1 | $ li |
定义好了别名就可以随时在 shell 中使用,但目前仅在当前进程中才有效,在子 shell 或新 shell 中使用别名无效。可以使用 ps -f
查看 shell 进程 ID。
外部命令
外部命令是存在于 bash shell 之外的程序,它们不是 shell 程序的一部分,外部命令程序通常位于 /bin
、 /usr/bin
、 /sbin
、 /usr/sbin
中。 ps
就是一个外部命令,使用 type查看如下所示:
1 | $ type ps |
外部命令会创建出一个包含全新环境的子进程
外部命令会在一个 新创建的、包含全新环境的 子进程中执行 (是否是 SubShell 进程呢?)。ps
的父进程是 bash shell PID 25838,如下所示:
1 | $ ps -f |
环境变量
在 Linux 中,很多程序和脚本都通过环境变量来获取系统信息,存储临时数据,配置信息。环境变量是指用来存储有关 shell 会话和工作环境信息,允许你在内存中存储数据,以便程序或 shell 中运行的脚本能够轻松访问到它们。也是存储持久数据的一种简便方法
在 bash shell 中,环境变量分为两类:
- 全局环境变量:对所有 shell 都可见(包括当前 shell、生成的 SubShell 和 ChildShell)。全局环境变量让那些 SubShell 和 ChildShell 需要获取父 shell 信息的程序来说非常有用。
- 局部环境变量:对当前 shell、生成的 SubShell 可见,对生成的 ChildShell 不可见。尽管它们是局部的,但是和全局环境变量一样重要。
系统全局环境变量
又叫默认全局环境变量。它们基本都是使用全大写字母,以区别普通用户的环境变量。
env
命令查看系统全局环境变量:
1 | $ env |
系统局部环境变量
在 linux 操作系统上,有默认全局环境变量,也有默认局部环境变量,用户也可以自定义局部变量。
不同的 shell 启动方式加载的脚本有所不同,有些脚本会包含局部变量。例如启动交互式 shell 时必须读取的 ~/.bashrc
文件,其中包含 alias 类型的命令,这些可以理解为局部环境变量。
1 | alias rm='rm -i' |
后者使用 alisa
命令查看所有的别名命令:
1 | alias |
测试局部变量的有效范围:
1 | ps -f |
查看局部环境变量的列表有点复杂。遗憾的是,在Linux系统并没有一个只显示局部环境变量的命令。使用 set 命令 会显示某个特定进程设置的所有环境变量,包括局变变量、全局变量、用户自定义变量。
自定义变量
规则:变量名区分大小写,为了与大写的系统环境变量区分,自定义变量和shell脚本推荐使用小写字母。
父 shell 中定义的局部变量,SubShell 可见,ChildShell 不可见。
父 shell 中定义的全局变量,所有子进程中都可见。
在子进程中 新增、删除、修改 局部变量和全局变量,都不会反映给父进程,这种改变仅在子进程中有效。
只要创建自定义局部变量和全局变量的进程一结束,该变量就会永久失效。
自定义局部变量
1 | $ my_variable="hello world" # 如果变量含有空格,则必须加单引号或双引号 |
自定义全局变量
先创建一个局部变量,然后通过 export
导出到全局环境,变量名前不加 $
。如下所示:
1 | $ echo $my_variable |
删除环境变量
删除环境变量用 unset
命令,变量名前不加 $
。
1 | $ echo $my_variable |
默认环境变量(系统环境变量)
默认情况下, bash shell会用一些特定的环境变量来定义系统环境。这些变量在你的Linux系统上都已经设置好了,只管放心使用。 bash shell源自当初的Unix Bourne shell,因此也保留了Unix Bourne shell里定义的那些环境变量。
bash shell提供的与Unix Bourne shell兼容的环境变量
常用的几个:
1 | HOME # 当前用户的主目录 |
使用 echo
输出变量 PATH 的值,会列出 shell 查找命令会去的6个目录:
1 | $ echo $PATH |
bash shell 自有变量
部分自有变量:
1 | BASH_SUBSHELL 当前子shell环境的嵌套级别 |
下面查看 bash shell 版本号:
1 | $ echo $BASH_VERSION |
PATH 环境变量
PATH 变量的作用是:当在 shell 命令行中输入一个外部命令时,shell 必须搜索对应的命令和程序,PATH 环境变量定义了搜索命令和程序的目录。
如果命令或者程序的位置没有包括在PATH变量中,那么不使用绝对路径的话,shell 就无法找到该命令。
例如 /home/nginx/sbin/nginx -v
,否则 shell 无法找到。
如果想在虚拟目录任意位置操作 nginx
服务,又不想每次都用 cd 绝对路径定位。有三种方法:
使用绝对路径。例如
/home/nginx/sbin/nginx -v
软链接 。把
nginx
的绝对路径通过软链接指定到 PATH 环境的一个目录下。添加 path 环境变量。PATH中各个目录之间是用冒号分隔,无需从头定义,只需引用原来的PATH值,然后再给这个字符串添加新目录就行了。
1
PATH=$PATH:/home/nginx/sbin/ # 使用export导出全局变量,可供ChildShell等子进程使用。
注意:即便导出为全局变量,这样的修改也只能持续到退出终端(exit)或重启系统。后面介绍如何永久保持环境变量的修改效果。
Windows系统同理,有些程序提供了 Protable edition
便携式版,下载并解压,将 bin 目录添加至 path 环境变量,这样就可以在 cmd 窗口任意目录下运行程序。例如 git、nssm。
shell 启动方式
当使用 bash shell 时,bash 是如何查找环境变量位置的?系统环境变量文件(启动文件或环境文件)会在哪些位置?
bash 检查的启动文件(环境文件)取决于你启动 bash shell 的方式。 Shell 四种运行方式(启动方式) :
- 交互式的登录 Shell;
- 交互式的非登录 Shell;
- 非交互式的登录 Shell;
- 非交互式的非登录 Shell。
交互式 shell
就是在终端上执行。终端等待命令的输入,当我们输入命令并提交后,shell 会接受到终端传来的命令并立即解释执行。这种模式被称作交互式是因为 shell 与用户进行交互。这种模式也是大多数用户非常熟悉的:登录、执行一些命令、退出。当你退出后,shell 也终止了。
启动交互式 shell
1、打开桌面环境自带的终端启动的 shell。
2、在命令行提示符下敲入 bash
启动的 shell。如果 bash 是作为交互式 shell 启动的,它就不会访问 /etc/profile
文件,只会检查用户 HOME 目录中的 .bashrc
文件。
配置文件
交互式 shell 必然读取 ~/.bashrc
文件。 该文件的两个作用:
1、读取 /etc
目录下通用的 bashrc
文件。具体请查看 .bashrc
文件中的内容,我的 Ubuntu 18.04 并没有这样写。
2、为用户提供一个定制自己的命令别名和私有脚本函数。 .bashrc
文件部分内容如下所示:
1 | ... |
非交互式 shell
以 shell script(非交互)方式执行。在这种模式下,shell 不与你进行交互,而是读取存放在文件中的命令,并且执行它们。当它读到文件的结尾 EOF,shell 也就终止了。
启动非交互式 shell
1、执行 shell 脚本,即启动一个 ChildShell。如果父 shell 是登录 shell,那么 ChildShell 可以从 /etc/profile
、/etc/profiled/*.sh
、$HOME/.bashrc
文件中继承这些变量。如果父 shell 中的变量是局部变量时,那么 ChildShell 无法继承。
配置文件
非交互式 shell 读取的是环境变量 BASH_ENV
(通常情况下)所指定的配置文件。
当 shell 启动一个非交互式 shell 进程时,它会检查这个环境变量来查看要执行的启动文件,默认情况下并没有设置该变量:
1 | $ printenv BASH_ENV |
登录式 shell
启动登陆式 shell
1、用户登陆时,输入用户名和密码后启动的 shell(例如使用 ssh 登录远程主机等),登录shell属于交互式shell。
2、通过带 --login
参数的命令(如:bash --login
)而启动的 shell。
配置文件
当登录 linux 系统时,bash shell 作为登录 shell 启动,登录 shell 会从 5 个不同的启动文件里读取命令:
/etc/profile
$HOME/.bash_profile
$HOME/.bashrc
$HOME/.bash_login
$HOME/.profile
而 /etc/profile
文件是系统默认的 bash shell 的主启动文件,系统上的每个用户登录时都会执行这个启动文件。另外 4 个启动文件是针对用户的(不同发行版中,四个启动文件一般都只有一到二个),可根据个人需求定制。
/etc/profile
文件
只要登录了 linux 系统,bash 就会执行 /etc/profile
启动文件中的命令(不同发行版的 /etc/profile
有不同的设置和命令),profile 文件内容如下:
1 | $ cat /etc/profile |
其中有个 for 语句,它用来循环读取 /etc/profile.d
目录下的所有文件。在我使用的 Ubuntu 中,该文件目录包含以下文件:
1 | $ cd /etc/profile.d/ |
启动文件中 sh
后缀供 bash shell 使用。若有 csh
后缀的,是供 c shell 使用。
$HOME
目录下的4个启动文件
仅对 $HOME 用户有效
在 $HOME
目录下的隐藏文件中可以找到 4 个启动文件中的一个到两个,每个用户都可以编辑这些文件添加自己的环境变量,这此环境变量会在每次启动 bash shell 会话时生效。HOME 目录如下:
1 | $ ls -la |
$HOME/.bash_profile
有的发行版会有 .bash_profile
启动文件,该启动文件会先去检查 HOME 目录下是不是有一个叫 .bashrc
的启动文件,如果有就会先执行里面的命令。.bash_profile
文件内容如下图所示:
可以看到,在 .bash_profile
文件中给 PATH 变量添加了一个目录路径,并用 export PATH
导入到全局中,又因为登录 shell 时会从该启动文件中读取命令,因此,在 .bash_profile
文件中加的变量可以持久化。
非登录式 shell
在图形界面中打开新终端、使用 su 切换用户、bash 命令创建的 ChildShell,均属于交互式非登录 shell。
判断启动方式
判断 shell 是否是交互式
判断是否为交互式 Shell 有两种简单的方法。
- 查看变量
-
的值,如果值中包含了字母i
,则表示交互式(interactive)。
【实例1】在 CentOS GNOME 桌面环境自带的终端下输出-
的值:
1 | echo $- |
包含了i
,为交互式。
【实例2】在 Shell 脚本文件中输出-
的值:
1 | cat test.sh |
不包含i
,为非交互式。注意,必须在新进程中运行 Shell 脚本。
- 查看变量
PS1
的值,如果非空,则为交互式,否则为非交互式,因为非交互式会清空该变量。
【实例1】在 CentOS GNOME 桌面环境自带的终端下输出 PS1 的值:
1 | echo $PS1 |
非空,为交互式。
【实例2】在 Shell 脚本文件中输出 PS1 的值:
1 | cat test.sh |
空值,为非交互式。注意,必须在新进程中运行 Shell 脚本。
判断 shell 是否为登录式
判断 Shell 是否为登录式也非常简单,只需执行shopt login_shell
即可,值为on
表示为登录式,off
为非登录式。
shopt 命令用来查看或设置 Shell 中的行为选项,这些选项可以增强 Shell 的易用性。
【实例1】在 CentOS GNOME 桌面环境自带的终端下查看 login_shell 选项:
1 | shopt login_shell |
【实例2】按下Ctrl+Alt+Fn
组合键切换到虚拟终端,输入用户名和密码登录后,再查看 login_shell 选项:
1 | shopt login_shell |
【实例3】在 Shell 脚本文件中查看 login_shell 选项:
1 | cat test.sh |
同时判断交互式、登录式
要同时判断是否为交互式和登录式,可以简单使用如下的命令:
echo $PS1; shopt login_shell
或者
echo $-; shopt login_shell
( ) 属于哪种启动方式?
进程列表的方式启动 ( )
,SubShell 会继承父 Shell 的交互和登录属性,所以 () 的启动方式取决于父 shell:
1 | (echo $PS1;shopt login_shell) |
常见的启动方式
交互式登录式
1、通过 Linux 控制台(不是桌面环境自带的终端)或者 ssh 登录 shell :
1 | echo $PS1;shopt login_shell |
2、使用 su -
切换用户,改变 shell 环境变量。
3、输入 bash --login
:
1 | bash --login |
交互式非登录式
1、在 Linux 桌面环境下打开终端
2、使用 su
切换用户
3、输入 bash
:
1 | bash |
非交互式登录式
非交互式非登录式
1、shell 脚本
1 | cat test.sh |
2、ssh 执行远程命令,但不登录。
1 | ssh localhost 'echo $PS1;shopt login_shell' |
环境变量持久化
上面了解了各种 shell 进程对应的环境文件,那么找出永久性环境变量就容易多了。可以利用这些文件来创建自己的永久性全局变量或局部变量。全局变量是对所有用户都需要使用的变量,可以将新的变量或修改过的变量设置放在 /etc/profile
文件中,但升级了发行版该文件也会更新,所以这点要注意 (对所有用户)。
最好是在 /etc/profile.d
目录中创建一个以 .sh
结尾的文件,把所有新的变量或修改过的变量全部放在此文件中(对所有用户)。
对于存储个人用户永久性 bash shell 变量的地方是 $HOME/.bashrc
文件。这一点适用于所有类型的 shell 进程(仅对当前用户)。
例:将 nginx 服务的绝对路径,添加到 PATH 全局环境变量中
1 | $ echo $PATH |