2019独角兽企业重金招聘Python工程师标准>>>
《Linux命令行与shell脚本编程大全》理解Shell——第五章
- 探究Shell的类型
- 理解Shell的父/子关系
- 别出心裁的子Shell用法
- 探究内建的Shell命令
5.1Shell的类型
系统启动什么样的Shell程序取决你与个人的用户ID配置,在/etc/passwd文件中,在用户ID记录的第7个字段中列出了默认的shell程序,只要用户登录到某个虚拟机终端,默认的shell程序就会开始运行。
cat /etc/passwd
work:x:600:1000::/home/work:/bin/bash
bash shell 程序位于/bin目录内,从长列表中可以看出/bin/bash(bash shell) 是一个可执行程序:
-rwxr-xr-x 1 root root 960472 Dec 7 2016 /bin/bash*
还有些 、/bin/tcsh /bin/dash /bin/csh
这些shell程序各自都可以被设置成用户的默认Shell
默认的交互shell会在用户登录某个虚拟控制台终端或在GUI中运行终端仿真器时启动, 不过还有另外一个默认shell是 /bin/sh 它作为默认的系统shell 用于那些需要在启动时使用的系统shell脚本。
经常会看到某些发行版使用软连接将默认的系统shell设置为bash shell
lrwxrwxrwx 1 root root 4 Aug 18 11:57 /bin/sh -> bash
对于bash shell脚本来说,这两种不同的shell(默认的交互shell 和默认的系统shell)会造成问题,一定要阅读第11章有关bash shell 脚本首行的语法要求,以避免这些麻烦
并不是必须使用默认的交互shell,可以使用发行版中可用的shell ,只需要输入对应的文件名即可。例如你可以直接输入命令/bin/dash 来启动dash shell
$/bin/dash
也可以使用exit来退出 dash shell
$exit
5.2 Shell 的父子关系
用于登录某个虚拟机在控制终端或在GUI中运行仿真器时所启动的默认的交互shell,是一个父shell,本书到目前为止都是父shell提供CLI提示符,然后等待命令输入。
在CLI提示符后输入/bin/bash命令或其他等效的bash命令时,会创建一个新的shell程序。设个shell程序被称之为子shell(child shell)。子shell也拥有CLI提示符,同样会等待命令输入。
当输入bash ,生成shell的时候,你是看不到任何相关信息的,因此需要另外一条命令帮我们理清这一切,ps -f
[work@16-11-119 ~]$ ps -f
UID PID PPID C STIME TTY TIME CMD
work 20181 20180 0 20:53 pts/2 00:00:00 -bash
work 20753 20181 0 21:15 pts/2 00:00:00 ps -f
[work@16-11-119 ~]$ bash
[work@16-11-119 ~]$ ps -f
UID PID PPID C STIME TTY TIME CMD
work 20181 20180 0 20:53 pts/2 00:00:00 -bash
work 20754 20181 0 21:15 pts/2 00:00:00 bash
work 20769 20754 0 21:15 pts/2 00:00:00 ps -f
输入命令bash之后,一个子shell就出现了,第二个ps -f 是在子shell中执行的,可以从现实结果中看到有两个bash shell程序在 运行。
父shell 发出命令 bash ——> 创建子shell
子shell 发出命令 : ps -f
在生成子shell进程时,只有部分父进程的环境被复制到子shell环境中。这会对包括变量在内的一些东西造成影响。
子shell 可以从父shell 中创建,也可以从另一个子shell中创建。
[work@16-11-119 ~]$ ps -f
UID PID PPID C STIME TTY TIME CMD
work 20181 20180 0 20:53 pts/2 00:00:00 -bash
work 20916 20181 0 21:22 pts/2 00:00:00 ps -f
[work@16-11-119 ~]$ bash
[work@16-11-119 ~]$ bash
[work@16-11-119 ~]$ bash
[work@16-11-119 ~]$ ps --forest
PID TTY TIME CMD
20181 pts/2 00:00:00 bash
20921 pts/2 00:00:00 \_ bash
20936 pts/2 00:00:00 \_ bash
20951 pts/2 00:00:00 \_ bash
20969 pts/2 00:00:00 \_ ps
上面的例子中bash 命令被输出了三次,这实际上创建了三个子shell, ps --forest 命令展示了这些子shell间的嵌套结构。
ps -f 也能表现子shell的嵌套关系。因为它能够通过PPID 列展示谁是谁的父进程。
exit 命令不仅能退出子shell 还能用来登出当前虚拟控制台或中断仿真器软件。只需要在父shell 中输入exit,就能够从容退出CLI了
5.2.1 进程列表
可以在一行汇总指定要运行的一系列命令。这可以通过命令列表来实现,只需要在命令之间加入分号(;)即可。
[work@16-11-118 ~]$ pwd;ls;cd /etc;pwd
/home/work
backup hs_err_pid30165.log hs_err_pid30207.log hs_err_pid30223.log hs_err_pid30239.log views
dubbo-registry hs_err_pid30172.log hs_err_pid30211.log hs_err_pid30227.log index.jsp
git.1.gz hs_err_pid30179.log hs_err_pid30215.log hs_err_pid30231.log META-INF
hs_err_pid30161.log hs_err_pid30189.log hs_err_pid30219.log hs_err_pid30235.log qf-pgc-provider-registry
/etc
上面的例子中,所有的命令依次执行,不存在任何问题。不过这并不是进程列表。命令列表想要成为进程列表,这些命令必须包含在括号里。
(pwd;ls;cd /etc;pwd)
尽管多出来没有什么太大的不同,但效果非同寻常。括号的加入使命令列表编程了进程列表,生成了一个子shell 来执行对应的命令
进程列表是一种命令分组,另一种命令分组是将命令放入花括号中,并在命令列表尾部加上分号(;),语法为{ command;}。使用花括号进行命令分组并不会像进程列表那样创建出子shell
要想知道是否生成了子shell,可以借助一个使用了环境变量的命令。这个命令是 echo $BASH_SUBSHELL。 如果命令返回0 则表明没有子shell 。如果返回1 或者其他 则代表存在子shell.
[work@16-11-118 ~]$ pwd;ls;cd /etc;pwd;cd ;pwd; echo $BASH_SUBSHELL
/home/work
backup hs_err_pid30165.log hs_err_pid30207.log hs_err_pid30223.log hs_err_pid30239.log views
dubbo-registry hs_err_pid30172.log hs_err_pid30211.log hs_err_pid30227.log index.jsp
git.1.gz hs_err_pid30179.log hs_err_pid30215.log hs_err_pid30231.log META-INF
hs_err_pid30161.log hs_err_pid30189.log hs_err_pid30219.log hs_err_pid30235.log qf-pgc-provider-registry
/etc
/home/work
0
[work@16-11-118 ~]$
[work@16-11-118 ~]$ (pwd;ls;cd /etc;pwd;cd ;pwd; echo $BASH_SUBSHELL)
/home/work
backup hs_err_pid30165.log hs_err_pid30207.log hs_err_pid30223.log hs_err_pid30239.log views
dubbo-registry hs_err_pid30172.log hs_err_pid30211.log hs_err_pid30227.log index.jsp
git.1.gz hs_err_pid30179.log hs_err_pid30215.log hs_err_pid30231.log META-INF
hs_err_pid30161.log hs_err_pid30189.log hs_err_pid30219.log hs_err_pid30235.log qf-pgc-provider-registry
/etc
/home/work
1
最后显示出了1 表明的确创建了子shell ,并用于执行这些命令。
所以说命令列表就是使用括号包围起来的一组命令,它能够创建出子shell来执行这些命令,甚至可以在命令列表中嵌套括号来创建子shell的子shell。
[work@16-11-118 ~]$ (pwd;(echo $BASH_SUBSHELL))
/home/work
2
[work@16-11-118 ~]$
在shell脚本中经常使用子shell进行多进程处理,但是采用子shell的成本不菲,会明显拖慢处理速度,在交互式的CLI Shell会话中,子shell同样存在问题,它并非真正的多进程处理,因为终端控制着子shell的I/O。 (sleep 10) 可以验证
5.2.2 别出心裁的子shell 用法
在交互式的shell CLI中,还有很多富有成效的子shell用法。 进程列表、协程和管道(11章会讲到) 都利用了子shell 。它们都可以有效地在交互式shell中使用。
在交互式shell中,一个搞笑的子shell 用法就是使用后台模式。
1.探索后台模式
在后台模式中运行命令可以在处理命令的同事让出CLI ,以供他用。 演示后台模式的一个命令就是sleep。
sleep命令接收一个参数,改参数是你希望进程等待(睡眠)的秒数。这个命令在脚本中常用于引入一段时间的暂停。 命令sleep 10 会将会话暂停10 秒钟,然后返回shell CLI 提示符。
[work@16-11-118 ~]$ sleep 10
[work@16-11-118 ~]$
要想将命令置于后台模式,可以在命令末尾加上字符 & ,把 sleep 命令置于后台模式可以让我们利用ps命令来小窥一番。
[work@16-11-118 ~]$ sleep 10 &
[1] 705
[work@16-11-118 ~]$ ps -f
UID PID PPID C STIME TTY TIME CMD
work 571 570 0 09:18 pts/5 00:00:00 -bash
work 705 571 0 09:34 pts/5 00:00:00 sleep 10
work 709 571 0 09:34 pts/5 00:00:00 ps -f
[work@16-11-118 ~]$
sleep 命令会在后台(&) 睡眠3000秒, 当它被置于后台,在shell CLI 提示符返回之前,会出现两条信息,第一条信息是显示在方括号中的后台作业(background job) 号 (1)。 第二条是后台作业的进程ID (705)。
ps 命令用来显示各种进程。我们可以注意到命令sleep 3000 已经被累出来了,第二列显示的进程ID(PID)
除了ps命令,你也可以使用jobs命令来显示后台作业信息,jobs命令可以显示出当前运行后台模式中的所有用户的进程(作业)。
[work@16-11-118 ~]$ sleep 10 &
[1] 734
[work@16-11-118 ~]$ jobs
[1]+ Running sleep 10 &
显示了作业号和 作业当前状态以及对应的命令。jobs -l 能够显示更多相关的信息。 -l还能够显示出命令的pid。
[work@16-11-118 ~]$ jobs -l
[2]+ 750 Running sleep 10 &
一旦作业完成,就会显示出结束状态
[work@16-11-118 ~]$ jobs
[2]+ Done sleep 10
后台作业的结束状态未必会一致等待何时的时候才现身。当作业结束状态突然出现在屏幕上的时候,可别吃惊。
2. 将进程列表置于后台
之前说过,进程列表是运行在子shell中的一条或多条命令, 将相同的继承列表置于后台模式会在命令输出上输出变量BASH_SHELL 结果和期望的一样。
[work@16-11-118 ~]$ (sleep 2;echo $BASH_SUBSHELL;sleep 2)&
[1] 832
[work@16-11-118 ~]$ 1
[1]+ Done ( sleep 2; echo $BASH_SUBSHELL; sleep 2 )
[work@16-11-118 ~]$
在CLI 中运行子shell的创造性方法之一,就是将进程列表置于后台模式。你既可以在子shell中进行繁重的处理工作,同时也不会让子shell的I/O 受制于终端。
使用tar 创建备份文件是有效利用后台进程列表的一个更实用的例子。
[work@16-11-119 home]$ (tar -czf work.tar.gz /home/work/work ; tar -czf abc.tar.gz /home/work/kk )&
将进程列表置入 后台模式并不是子shell在 CLI中仅有的创造性用法,携程就是另一种用法。
3.携程
携程可以同时做两件事。它在后台生成一个子shell,并在这个子shell 中执行。
要进行携程处理,得使用coproc命令,还有要在子shell中执行命令。
[work@16-11-119 home]$ coproc sleep 10
[1] 51574
除了会创建子shell之外,携程基本上就是将命令置入后台模式。当输入coproc命令及其参数之后,你会发现启用一个后台作业。屏幕上会显示出后台作业号(1)以及进程ID(2544)
jobs命令能够显示出携程的处理状态
[work@16-11-119 home]$ jobs
[2]+ Running coproc COPROC sleep 10 &
从上面例子中可以看到在子shell中执行的后台命令是 coproc COPROC sleep 10 。COPROC 是 coproc 命令给进程起的名字。你可以使用命令的扩展语法自己设置这个名字。
[work@16-11-119 home]$ coproc My_Job { sleep 10; }
[3] 51714
[work@16-11-119 home]$ jobs
[3]+ Running coproc My_Job { sleep 10; } &
通过使用扩展语法,携程的名字被设置成My_Job。这里要注意的是,扩展语法写起来有点麻烦。
必须确保在第一个花括号 ({)和命令名之间有一个空格。还必须保证命令以分号(;) 结尾。另外 分号和闭花括号(}) 之间也得有一个空格。
可以将协程与进程列表结合起来产生嵌套的子shell。 只需要输入进程列表,然后把命令coproc 放在前面就行了。
[work@16-11-119 home]$ coproc ( sleep 10;sleep 2)&
[1] 51993
[work@16-11-119 home]$ jobs
[1]+ Running coproc COPROC ( sleep 10; sleep 2 ) &
[work@16-11-119 home]$ ps --forest
PID TTY TIME CMD
51292 pts/2 00:00:00 bash
51993 pts/2 00:00:00 \_ bash
51994 pts/2 00:00:00 | \_ sleep
51997 pts/2 00:00:00 \_ ps
[work@16-11-119 home]$
下一节我们将研究内建命令和外部命令之间的行为差异
5.3理解Shell内建命令
搞明白shell的内建命令和非内建(外部)命令非常重要。内建命令和非内建命令的操作方式大不相同。
5.3.1 外部命令
外部命令有时候被称为文件系统命令,是存在于bash shell之外的程序。它们并不是shell程序汇总的一部分,外部命令程序通常位于/bin、/usr/bin、/sbin或 /usr/sbin中。
ps就是一个外部命令,你可以使用which 和 type命令找到它
[work@16-11-119 home]$ which ps
/usr/bin/ps
[work@16-11-119 home]$ type -a ps
ps is /usr/bin/ps
ps is /bin/ps
[work@16-11-119 home]$ ls -l /usr/bin/ps
-rwxr-xr-x. 1 root root 100120 Nov 6 2016 /usr/bin/ps
[work@16-11-119 home]$
外部命令执行时,会创建一个子进程。这种操作被称为衍生(forking)。 外部命令ps很方便显示出它的父进程以及自己所对应的衍生子进程。
[work@16-11-119 home]$ ps -f
UID PID PPID C STIME TTY TIME CMD
work 51292 51291 0 22:10 pts/2 00:00:00 -bash
work 52599 51292 0 23:11 pts/2 00:00:00 ps -f
作为外部命令。ps 命令执行时会创建出一个子进程。可以看出它的父子关系。
当进程必须执行衍生操作时,它需要花费时间和精力来设置新子进程的环境。所以说,外部命令多少还是有代价的。
就算衍生出子进程或是创建了子shell,你仍然可以通过发送信号与其沟通,这一点无论是在命令行还是在脚本编写中都是机器有用的。发送信号(signaling)使得进程间可以通过信号进行通讯,信号及其发送会在第16章中讲到。
5.3.2 内建命令
内建命令和外部命令的区别在于前者不需要使用子进程来执行,他们已经和shell编译成了一体,作为shell 工具的组成部分存在。不需要借助外部程序文件来运行。
cd 和 exit 命令都内建于 bash shell 。 可以利用type命令来了解某个命令是否是内建的。
[work@16-11-119 home]$ type cd
cd is a shell builtin
[work@16-11-119 home]$ type ps
ps is hashed (/usr/bin/ps)
[work@16-11-119 home]$ type exit
exit is a shell builtin
因为既不需要衍生出子进程来执行,也不需要打开程序文件,内建命令的执行速度要更快,效率也更高。
注意:有些命令有多种实现。例如echo 和 pwd 既有内建命令也有外部命令,两种实现略有不同。要查看命令的不同实现,使用 type 命令的 -a 选项。
[work@16-11-119 home]$ type -a echo
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo
[work@16-11-119 home]$ type echo
echo is a shell builtin
[work@16-11-119 home]$ which echo
/usr/bin/echo
type -a 显示出了每个命令的两种实现 注意,which 命令只显示出了外部命令文件。
对于多种实现的命令,如果想要使用其外部命令实现,直接指明对应的文件就可以了。例如,要使用外部命令pwd , 可以输入/bin/pwd
1.使用history 命令
一个有用的内建命令是history 命令。bash shell 会跟踪你用过的命令,你可以换回这些命令并重新使用。
可以设置保存在bash历史记录中的命令数,要想实现这一点,你需要修改名为HISTSIZE的环境变量
可以换回并重用历史列表中最近的命令,这样能够节省时间和击键量。输入!! ,然后按回车键就能够换出刚刚用过的那条命令来使用。
[work@16-11-119 home]$ ps --forest
PID TTY TIME CMD
51292 pts/2 00:00:00 bash
53360 pts/2 00:00:00 \_ ps
[work@16-11-119 home]$ !!
ps --forest
PID TTY TIME CMD
51292 pts/2 00:00:00 bash
53362 pts/2 00:00:00 \_ ps
[work@16-11-119 home]$
当输入!!时,bash 首先会显示出从shell的历史记录中换回的命令,然后执行该命令。
历史记录保存在隐藏文件.bash_history中,它位于用户的主目录中。需要注意的是,bash命令的历史记录是先存放在内存中。当shell 退出时才被写入到历史文件中。
要实现强制写入,需要使用history 命令的 -a选项。
如果打开了多个终端会话,仍然可以使用history -a命令 在打开的本次会话中向.bash_history文件中添加记录,但是对于其他打开的终端会话,历史记录(history的输出)并不会更新。因为.bash_history文件只有在打开首个终端会话时才会被读取,要想强制重新读取.bash_profile文件,更新终端会话的历史记录,可以使用history -n 命令。
我们可以唤回历史记录列表汇总任意一条命令,只需输入惊叹号,和历史列表中的编号即可。
[work@16-11-119 ~]$ history
...
987 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:33:24 :clear
988 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:33:24 :ls
989 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:35:08 :cd web/
990 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:35:09 :ls
991 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:35:10 :clear
992 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:35:11 :ls
993 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:35:18 :cd crm-pms/
994 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 10:35:20 :ls
995 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 12:53:13 :history
996 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 12:55:52 :clear
997 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 12:55:52 :ls
998 work pts/2 2017-11-17 12:53 (172.16.6.84)_2017-11-17 12:55:55 :history
[work@16-11-119 ~]$ !996
clear
[work@16-11-119 ~]$
编号为996的命令从历史记录中被取出,和执行最近的命令一样,bash shell 首先显示出从shell 历史记录中换回的命令,然后执行该命令。
2.命令别名
alias 是另一个shell的内建命令。命令别名允许你为常用命令(及参数) 创建另一个名称,从而将输入量减少到最低。
alias -p 查看当前可用的别名。
[work@16-11-119 ~]$ alias -p
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias vi='vim'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
定义好的别名,可以随时在shell中使用它,就算shell脚本中也没问题。 需要注意的是,因为命令别名属于内部命令,一个别名仅在它被定义的shell进程中才有效。
$ alias li='ls -li'
$
$ bash
$
$ li
bash: li : command not found
$
$ exit
exit
$
5.4 小结
本章讨论了复杂的交互程序:GNU bash shell。 其中包括理解shell进程及其关系,如何生成子shell,以及子shell, 以及子shell与父 shell的关系。 还探究了那些能够创建子进程的命令 和 不能创建子进程的命令。
当用户登录终端的时候,通常会启动一个默认的交互式 shell 。系统究竟启动哪个shell,这取决于用户ID配置。一般这个shell 都是/bin/bash。 默认的系统shell (/bin/sh) 用于系统shell 脚本,如那些需要在系统启动时运行的脚本。
子shell可以利用bash命令来完成。当使用进程列表或coproc 命令时也会产生子shell。将子shell运用在命令行中使得我们能够创造性地高效使用CLI。子shell还可以嵌套。生成子shell的子shell。创建子shell的代价可不低,因为还必须为子shell创建出一个全新的环境。
在最后,我们学习了两种不同类型的命令:内建命令和外部命令。外部命令会创建出一个包含全新环境的子进程,而内建命令则不会。相比之下外部命令使用成本更高。内建命令因为不会创建新环境,所以更搞笑,不会受到环境变化的影响。
shell、子shell 、进程和衍生进程都会受到环境变量的影响。下章将研究环境变量的影响方式以及如何在不同的上下文中使用环境变量。