Linux Shell的父子关系及内建命令

Linux Shell的父子关系及内建命令

Shell的类型

  系统启动什么样的shell程序取决于你个人的用户ID配置。在/etc/passwd文件中,在用户ID记录的第7个字段中列出了默认的shell程序。只要用户登录到某个虚拟控制台终端或是在GUI中启动终端仿真器,默认的shell程序就会开始运行。

  在下面的例子中,用户xiaoyu使用了GNU bash shell作为自己的默认shell程序:

1 :/$ cat /etc/passwd
2 [...]
3 xiaoyu:x:1000:1000:xiaoyu,,,:/home/xiaoyu:/bin/bash

  bash shell程序位于/bin目录内。从长列表中可以看出/bin/bash(bash shell)是一个可执行程序:

1 :/$ ls -lF /bin/bash
2 -rwxr-xr-x 1 root root 1037528 5月  16  2017 /bin/bash*

Shell的父子关系

:/$ ps -f
UID         PID   PPID  C STIME TTY          TIME CMD
xiaoyu     2448   2441  0 21:32 pts/1    00:00:00 bash
xiaoyu     3772   2448  0 22:38 pts/1    00:00:00 ps -f
:/$
:/$ bash
:/$ ps -f
UID         PID   PPID  C STIME TTY          TIME CMD
xiaoyu     2448   2441  0 21:32 pts/1    00:00:00 bash
xiaoyu     3781   2448  0 22:39 pts/1    00:00:00 bash
xiaoyu     3791   3781  0 22:39 pts/1    00:00:00 ps -f
:/$

  第一次使用ps -f的时候,显示出了两个进程。其中一个进程的进程ID是2448(第二列),运行的是bash shell程序(最后一列)。另一个进程(进程ID为3772)对应的命令是ps -f。

  说明:进程就是正在运行的程序。bash shell是一个程序,当它运行的时候,就成为了一个进程。一个运行着的shell就是某种进程而已。因此,在说到运行一个bash shell的时候,你经常会看到"shell"和”进程“这两个词交换使用。

  在输入命令bash之后,一个子shell就出现了。第二个ps -f是在子shell中执行的。可以从显示结果中看到有两个bash shell在运行。第一个bash shell程序,也就是父shell进程,其原始进程ID是2448。第二个bash shell程序,即子shell进程,其PID是3781。注意:子shell的父进程ID(PPID)是2448,指明了这个父shell就是该子shell的父进程。下图展示了这种关系。

 Linux Shell的父子关系及内建命令 

   在生成子shell进程时,只有部分父进程的环境被复制到子shell环境中。这会对包括变量在内的一些东西造成影响,后面我们会聊到这个。

  子shell(child shell,也叫subshell)可以从父shell中创建,也可以从另一个子shell中创建。

:/$ bash
:/$ 
:/$ bash
:/$ 
:/$ bash
:/$ 
:/$  8 :/$ ps --forest
   PID TTY          TIME CMD
  2448 pts/1    00:00:00 bash
  3781 pts/1    00:00:00  \_ bash
  3973 pts/1    00:00:00      \_ bash
  3989 pts/1    00:00:00          \_ bash
  3999 pts/1    00:00:00              \_ bash
  4035 pts/1    00:00:00                  \_ ps
:/$

  在上面的例子中,bash命令被输入了三次。这实际上创建了三个子shell(上面那步中我们已经输入一次bash,因此显示有4个)。ps --forest命令展示了这些子shell间的嵌套结构。下图展现了这种关系。

Linux Shell的父子关系及内建命令

   ps -f命令也能够表现子shell的嵌套关系,因为它能够通过PPID列显示出谁是谁的父进程。

:/$ ps -f
UID         PID   PPID  C STIME TTY          TIME CMD
xiaoyu     2448   2441  0 21:32 pts/1    00:00:00 bash
xiaoyu     3781   2448  0 22:39 pts/1    00:00:00 bash
xiaoyu     3973   3781  0 22:49 pts/1    00:00:00 bash
xiaoyu     3989   3973  0 22:49 pts/1    00:00:00 bash
xiaoyu     3999   3989  0 22:49 pts/1    00:00:00 bash
xiaoyu     4102   3999  0 22:54 pts/1    00:00:00 ps -f
:/$

   可以输入man bash程序可使用命令行参数修改shell启动方式。下表列举了bash中可用的命令行参数。

参数描述
-c string从string中读取命令并进行处理
-i启动一个能够接收用户输入的交互shell
-I以登录shell的形式启动
-r启动一个受限shell,用户会被限制在默认目录中
-s从标准输入中读取命令

   可以输入man bash获得关于bash命令的更多帮助信息,了解更多的命令行参数。bash --help命令也提供一些额外的协助。

  可以利用exit命令油条不紊地退出子shell。

:/$ exit
exit
:/$ ps --forest
   PID TTY          TIME CMD
  2448 pts/1    00:00:00 bash
  3781 pts/1    00:00:00  \_ bash
  3973 pts/1    00:00:00      \_ bash
  3989 pts/1    00:00:00          \_ bash
  4205 pts/1    00:00:00              \_ ps
:/$ 
:/$ exit
exit
:/$ ps --forest
   PID TTY          TIME CMD
  2448 pts/1    00:00:00 bash
  3781 pts/1    00:00:00  \_ bash
  3973 pts/1    00:00:00      \_ bash
  4218 pts/1    00:00:00          \_ ps
:/$ 
:/$ exit
exit
:/$ 
:/$ ps --forest
   PID TTY          TIME CMD
  2448 pts/1    00:00:00 bash
  3781 pts/1    00:00:00  \_ bash
  4227 pts/1    00:00:00      \_ ps
:/$ exit
exit
:/$ ps --forest
   PID TTY          TIME CMD
  2448 pts/1    00:00:00 bash
  4230 pts/1    00:00:00  \_ ps
:/$

   exit命令不仅能退出子shell,还能用来登出当前的虚拟控制台终端或终端仿真器软件。只需要在父shell中输入exit,就能够从容退出CLI了。

  运行shell脚本也能够创建出子shell。

  就算是不使用bash shell命令或是运行shell脚本,你也可以生成子shell。一种方法就是使用进程列表。

   进程列表

   可以在一行中指定要依次运行的一系列命令。这可以通过命令列表来实现,只需要在命令之间加入分号(;)即可。

:/$ pwd; ls; cd /etc; pwd; cd ; pwd; ls
/
bin   cdrom  dev  home        initrd.img.old  lib64       media  opt   root  sbin  srv  tmp  var
boot  core   etc  initrd.img  lib             lost+found  mnt    proc  run   snap  sys  usr  vmlinuz
/etc
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
:~$

   上面这个例子中,所有的命令依次执行,不存在任何问题。不过这并不是进程列表。命令列表想要成为进程列表,这些命令必须包含在括号里。

:~$ (pwd; ls; cd /etc; pwd; cd ; pwd; ls)
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
/etc
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
:~$

尽管多出来的括号看起来没有上面太大的不同,但起到的效果确实非同寻常。括号的加入使命令列表变成了进程列表,生成了一个子shell来执行对应的命令。

  说明:进程列表是一种命令分组(command grouping)。另一种命令分组是将命令放入花括号中,并在命令列表尾部加入分号(;)。语法为{command;}。使用花括号进行命令分组并不会像进程列表那样创建出子shell。

   要想知道是否生成了子shell,得借助一个使用了环境变量的命令(环境变量会在下一篇博文中写)。这个命令就是echo $BASH_SUBSHELL。如果该命令返回0,就表明没有子shell。如果返回1或者更大其他数字,就表明存在子shell。

   下面的例子中使用了一个命令列表,列表尾部是echo $BASH_SUBSHELL。

:~$ pwd; ls; cd /etc; pwd; cd ; pwd; ls; echo $BASH_SUBSHELL
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
/etc
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
0

   在命令输出的最后,显示的数字是0。这就表明这些命令不是在子shell中运行的。

  要是使用进程列表的话,结果就不一样了。在列表最后加入echo $BASH_SUBSHELL。

:~$ (pwd; ls; cd /etc; pwd; cd ; pwd; ls; echo $BASH_SUBSHELL)
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
/etc
/home/xiaoyu
Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  Templates  Videos
1
:~$

   这次在命令输入的最后显示了1。这表明的确创建了子shell,并用于执行这些命令。

  所以说,命令列表就是使用花括号包围起来的一组命令,它能创建出子shell来执行这些命令。

:~$ (pwd; echo $BASH_SUBSHELL)
/home/xiaoyu
1
:~$ (pwd; (echo $BASH_SUBSHELL))
/home/xiaoyu
2
:~$

   注意:在第一个进程列表中,数字1表明了一个子shell,这个结果和预期的一样。但是在第二个进程列表中,在命令echo $BASH_SUBSHELL外面又多出了一对括号。这对括号在子shell中产生了另一个子shell来执行命令。因此数字2表明的就是这个子shell。

  在shell脚本中,经常使用子shell进行多进程处理。但是采用子shell的成本不菲,会明显拖慢处理速度。在交互式的CLI会话中,子shell同样存在问题。它并非真正的多进程处理,因为终端控制着shell的I/O。

   别出心裁的子shell用法

   在交互式的shell CLI用法,还有很多更富有成效的子shell用法。进程列表、协程和管道都利用了子shell。他们都可以有效地交互式shell中使用。

  在交互式shell中,一个高效地子shell用法就是使用后台模式。在讨论如果将后台模式与子shell搭配使用之前,你得先搞明白什么是后台模式。

  1、探索后台模式

   在后台模式中运行命令命令可以在处理命令地同时让出CLI,以供它用。演示后台模式地一个经典命令就是sheep。

  sleep命令接受一个参数,该参数是你希望进程等待(睡眠)地秒数。这个命令在脚本中常用于引入一段时间地暂停。命令sleep 10会将会话暂停10秒钟,然后返回shell CLI提示符。

  要想将命令置入后台模式,可以在命令末尾加上字符&。把sheep命令置入后台模式可以让我们利用ps命令查看一番

:~$ sleep 10
:~$ 
:~$ sleep 3600&
[1] 5190
:~$ ps -f
UID         PID   PPID  C STIME TTY          TIME CMD
xiaoyu     2448   2441  0 21:32 pts/1    00:00:00 bash
xiaoyu     5190   2448  0 23:50 pts/1    00:00:00 sleep 3600
xiaoyu     5191   2448  0 23:51 pts/1    00:00:00 ps -f
:~$

   sleep命令会在后台(&)睡眠3000秒(50分钟)。当它被置入后台,在shell CLI提示符返回之前,会出现两条信息。第一条信息是显示在方括号总地后台作业(background job)号(1).第二条是后台作业地进程ID(5190)。

  ps命令用来显示各种进程。我们可以注意到命令sleep 3000已经被列出来了。在第二列显示的进程ID(PID)和命令进入后台时所显示PID是一样的,都是5190。

  除了ps命令,你也可以使用jobs命令来显示后台作业信息。jobs命令可以显示出当前运行在后台模式中的所有用户的进程(作业)。

1 :~$ jobs
2 [1]+  Running                 sleep 3600 &
3 :~$

   jobs命令在方括号中显示出作业号(1).它还显示了作业的当前状态(running)以及对应的命令(sleep 3000&)。

  利用jobs命令的-l(字母L的小写形式)选项,你还能够看到更多的相关信息。除了默认信息之外,-l选项还能够显示出命令的PID。

1 :~$ jobs -l
2 [1]+  5190 Running                 sleep 3600 &
3 :~$

   一旦后台作业完成,就会显示出结束状态。

:~$ sleep 5&
[2] 5435
:~$ jobs -l
[1]-  5190 Running                 sleep 3600 &
[2]+  5435 Done                    sleep 5
:~$

   需要提醒的是:后台作业的结束状态可未必会一直等待到合适的时候才现身。当作业结束状态突然出现在屏幕上的时候,你可别吃惊。

   后台模式非常方便,它可以让我们在CLI中创建出有实用价值的子shell。

  2、将进程列表置入后台

  进程列表是运行在子shell中的一条或多条命令。使用包含了sleep命令的进程列表,并显示出变量BASH_SUBSHELL,结果和期望的一样

1 :~$ (sleep 2; echo $BASH_SUBSHELL; sleep 2)
2 1
3 :~$

  在上面的例子中,有一个2秒钟的暂停,显示出的数字表明只有一个子shell,在返回提示符之前又经历了另一个2秒钟的暂停。没什么大事。

  将相同的进程列表置入后台模式会在命令输出上表现出些许不同。

:~$ (sleep 2; echo $BASH_SUBSHELL; sleep 2)&
[1] 4498
:~$ 1

:~$ ps -f
UID         PID   PPID  C STIME TTY          TIME CMD
xiaoyu     2906   2899  0 10:23 pts/6    00:00:00 bash
xiaoyu     4510   2906  0 11:45 pts/6    00:00:00 ps -f
[1]+  Done                    ( sleep 2; echo $BASH_SUBSHELL; sleep 2 )
:~$

  把进程列表置入后台会产生一个作业号和进程ID,然后返回到提示符。不过奇怪的是表明单一级子shell的数字1显示在了提示符的旁边!不要不知所措,只需要按一下回车键,就会得到另一个提示符。

  在CLI中运用子shell的创造性方法之一就是将进程列表置入后台模式。你既可以在子shell中进行繁重的处理工作,同时也不会让子shell的I/O受制于终端。

  sleep和echo命令的进程列表只是作为示例,使用tar创建备份文件是有效利用后台进程列表的一个更实用的例子。

:~$ (tar -cvf music.tar Music/; tar -cvf video.tar Videos/)&
[1] 4820
:~$ Music/
Videos/

[1]+  Done                    ( tar -cvf music.tar Music/; tar -cvf video.tar Videos/ )
:~$

  将进程列表置入后台模式并不是子shell在CLI中仅有的创造性方法。协程就是另一种方法。

  3、协程

  协程可以同时做两件事情。它在后台生成一个子shell,并在这个子shell中执行命令。

  要进行协程处理,得使用coproc命令,还有要在子shell中执行的命令。

:~$ coproc sleep 10
[1] 49403
:~$ jobs
[1]+  Running                 coproc COPROC sleep 10 &
:~$

  除了会创建子shell之外,协程基本上就是将命令置入后台模式。当输入coproc命令及其参数之后,你会发现启用了一个后台作业。屏幕上会显示出后台作业号(1)以及进程ID(49403)。

  jobs命令能够显示出协程的处理状态。

  从上面的例子中可以看到子shell中执行的后台命令是coproc COPROC sleep 10。COPROC是coproc命令给进程起的名字。你可以使用命令的扩展语法自己设置这个名字。

:~$ coproc my_jobs { sleep 10; }
[1] 49556
:~$ jobs
[1]+  Running                 coproc my_jobs { sleep 10; } &
:~$

  通过扩展语法,协程的名字被设置为my_jobs。需要注意:大括号{}之间,前后都有一个空格,而且命令以分号;结束。

  协程能够让我们尽情发挥想象力,发送或接受来自子shell中进程的信息。只有在拥有多个协程的时候才需要对协程进行命名,因为我们需要和它们进行通信。否则的话,让coproc命令将其设置成默认的名字coproc就行了。

可以尽情发挥,将协程与进程列表结合起来产生嵌套的子shell。只需要输入进程列表,然后把命令coproc放在前面就行了。

:~$ coproc friends { sleep 10; sheep 2; }
[1] 49667
:~$ jobs
[1]+  Running                 coproc friends { sleep 10; sheep 2; } &
:~$

  Remember:生成子shell的成本不低,而且速度还慢。创建嵌套子shell更是火上浇油!

  在命令中使用子shell能够获得灵活性和便利。要想获得这些优势,重要的是理解子shell的行为方式。对于命令也是如此,下面我们将看看内建命令与外部命令之间的行为差异。

理解shell的内建命令

  弄明白shell内建命令和非内建(外部)命令非常重要。内建命令和非内建命令的操作方式大不相同。

  外部命令

  外部命令,有时候也被称为文件系统命令,是存在于bash shell之外的程序。它们并不是shell程序的一部分。外部命令程序通常位于/bin、/usr/bin、/sbin、/usr/sbin中。

  ps就是一个外部命令。可以使用which和type命令找到它。

:~$ which ps
/bin/ps
:~$ type -a ps
ps is /bin/ps
:~$ ls -lF /bin/ps
-rwxr-xr-x 1 root root 97408 5月  14  2018 /bin/ps*
:~$

  当外部命令执行时,会创建出一个子进程。这种操作被称为衍生(forking)。外部命令ps很方便显示出它的父进程以及自己 所对应的衍生子进程。

:~$ ps -f
UID         PID   PPID  C STIME TTY          TIME CMD
xiaoyu     2906   2899  0 10:23 pts/6    00:00:00 bash
xiaoyu    50292   2906  0 14:30 pts/6    00:00:00 ps -f
:~$

  作为外部命令,ps命令执行时会创建出一个子进程。在这里,ps命令的PID是50292。PPID是2906。作为父进程的bash shell的PID是2906。下图展示了外部命令执行时的衍生过程。

Linux Shell的父子关系及内建命令

  当进程必须执行衍生操作时,它需要花费时间和精力来设置新子进程的环境。所以说,外部命令多少还是由代价的。

  说明:就算衍生出子进程或是创建了子shell,你仍然可以通过发送信号与其沟通,这一点无论是在命令行还是在脚本编写中都是极其有用的。发送信号(signaling)使得进程间可以通过信号进行通信。

  内建命令

  内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和shell编译成了一体,作为shell工具的组成部分存在。不需要借助外部程序文件来运行。

  cd和exit命令都内建于bash shell。可以利用type命令来了解某个命令是否是内建的。

:~$ type cd
cd is a shell builtin
:~$ type exit
exit is a shell builtin
:~$

  因为即不需要通过衍生出子进程来执行,也不需要打开程序文件,内建命令的执行速度要更快,效率也更高。

  需要注意的是:有些命令有多种实现。例如echo和pwd既有内建命令也有外部命令。两种实现略有不同。要查看命令的不同实现,使用type命令的-a选项。

:~$ type -a echo
echo is a shell builtin
echo is /bin/echo
:~$ which echo
/bin/echo
:~$ type -a pwd
pwd is a shell builtin
pwd is /bin/pwd
:~$ which echo
/bin/echo
:~$

  命令type -a会显示出每个命令的两种实现。注意:which命令只显示出了外部命令文件。

  对于有多种实现的命令,如果想要使用其外部命令实现,直接指明对应的文件就可以了。例如,要使用外部命令pwd,可以输入/bin/pwd。

  1、使用history命令

:~$ history 
 cd Downloads/
 ls -l
 tar zxvf VMwareTools-10.3.10-12406962.tar.gz 
 ls -l
 cd vmware-tools-distrib/
 ls
 sudo ./vmware-install.pl 
 sudo apt-get install open-vm-tools
 sudo apt-get install open-vm*
 sudo reboot
 cd /mnt/hgfs/
 ls
 cd share/
 ls
 ps -forest

这里我就只列出这么多,通常历史记录中会保存最近的1000条命令。这个数量一点都不少。

  可以设置保存在bash历史记录中的命令数。要想实现这一点,需要修改名为HISTSIZE的环境变量。

  可以唤回并重用历史列表中最近的命令。这样能够节省时间和击键量。输入!!,然后按回车键就能够唤出刚刚用过的那条命令来使用

:~$ ps --forest
   PID TTY          TIME CMD
  2906 pts/6    00:00:00 bash
 52030 pts/6    00:00:00  \_ ps
:~$ !!
ps --forest
   PID TTY          TIME CMD
  2906 pts/6    00:00:00 bash
 52033 pts/6    00:00:00  \_ ps
:~$

  当输入!!时,bash首先会显示出从shell的历史纪录中唤回的命令。然后执行该命令。

  命令历史记录被保存在隐藏文件.bash_history中,它位于用户的主目录中。这里要注意的是,bash命令的历史纪录是先存放在内存中,当shell退出时才被写入到历史文件中。

  可以在退出shell会话之前强制将命令历史记录写入.bash_history文件。要实现强制写入,需要使用history命令的-a选项。

说明 如果你打开了多个终端会话,仍然可以使用history -a命令在打开的会话中向.bash_history文件中添加记录。但是对于其他打开的终端会话,历史记录并不会自动更新。这是因为.bash_history文件只有在打开首个终端会话时才会被读取。要想强制重新读取.bash_history文件,更新终端会话的历史记录,可以使用history -n命令。

  可以唤回历史列表中任意一条命令。只需输入惊叹号和命令在历史列表中的编号即可。

$ !20 
type -a pwd 
pwd is a shell builtin 
pwd is /bin/pwd 
$

  2、命令别名

  alias命令是另一个shell的内建命令。命令别名允许你为常用的命令(及其参数)创建另一个名称,从而将输入量减少到最低。
  你所使用的Linux发行版很有可能已经为你设置好了一些常用命令的别名。要查看当前可用的别名,使用alias命令以及选项-p。

:~$ alias -p
alias alert=‘notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e ‘\‘‘s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//‘\‘‘)"‘
alias egrep=‘egrep --color=auto‘
alias fgrep=‘fgrep --color=auto‘
alias grep=‘grep --color=auto‘
alias l=‘ls -CF‘
alias la=‘ls -A‘
alias ll=‘ls -alF‘
alias ls=‘ls --color=auto‘
:~$

  可以使用alias命令创建属于自己的别名

:~$ alias li=‘ls -i‘
:~$ li
2752525 Desktop    2752526 Downloads         2752530 Music             2752531 Pictures  2752527 Templates  2752982 vmware-tools-distrib
2752529 Documents  2752517 examples.desktop  2755056 new_my_directory  2752528 Public    2752532 Videos     2752953 xiaoyu
:~$ bash
:~$ li
li: command not found
:~$ exit
exit
:~$ li
2752525 Desktop    2752526 Downloads         2752530 Music             2752531 Pictures  2752527 Templates  2752982 vmware-tools-distrib
2752529 Documents  2752517 examples.desktop  2755056 new_my_directory  2752528 Public    2752532 Videos     2752953 xiaoyu
:~$

  在定义好别名之后,随时都可以在shell中使用,就算再shell脚本中也没问题。要注意:因为命令别名属于内建命令,一个别名尽在它所被定义的shell进程中才有效。上例子中就是很好的说明。

相关推荐