linux shell 4. 理解shell

4.1 shell的类型

[root@Minimall ~]# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:997:User for polkitd:/:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
chrony:x:998:996::/var/lib/chrony:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
[root@Minimall ~]# 

在/etc/passwd文件中,用户ID记录的第7个字段列出了默认的shell程序,用户登陆虚拟控制台或在GUI中运行终端仿真器时,默认的shell程序就会开始运行。

当然,Linux系统里还有些shell程序,比如tcsh、dash、csh等等,这些shell程序都可以被设置成用户的默认shell,不过由于bash shell的广为流行,很少有人使用其他的shell作为默认shell。

Linux系统还有一个默认shell是/bin/sh,它作为默认的系统shell,用于那些需要在启动时使用的系统shell脚本。

[root@Minimall bin]# ls -l *sh
-rwxr-xr-x. 1 root root 960608 Sep  6 12:25 bash
-rws--x--x. 1 root root  23872 Aug  4 07:18 chsh
-rwxr-xr-x. 1 root root   4629 Aug  2 22:22 gettext.sh
-rwxr-xr-x. 1 root root  15776 Jul 24  2015 lchsh
-rwxr-xr-x. 1 root root   2291 Jul 30  2015 lesspipe.sh
-rwxr-xr-x. 1 root root   1539 Sep 29 08:51 setup-nsssysinit.sh
lrwxrwxrwx. 1 root root      4 Oct  2 06:27 sh -> bash
-rwxr-xr-x. 1 root root 778752 Sep  6 18:17 ssh
[root@Minimall bin]# ls -l /bin/sh
lrwxrwxrwx. 1 root root 4 Oct  2 06:27 /bin/sh -> bash

这里使用的发行版使用软连接将默认的系统shell(/bin/sh)设置为bash shell,有些版本也将默认的系统shell(/bin/sh)设置为dash shell。注意,默认的系统shell和默认的交互式shell并不相同。

只要输入对应的文件名,就可以使用发行版中所有可用的shell,可以直接输入命令/bin/dash来其他dash shell。使用exit可以退出当前shell,下面以其他shell为例:

[root@Minimall /]# /bin/lchsh
Changing shell for root.
New Shell [/bin/bash]: exit
Shell changed.
[root@Minimall /]#

注意:如果换了shell,退出后发现无法正常登录系统了,可以参考以下办法换回bash shell:

把 root 的默认 shell 换回 bash:
sudo chsh -s /bin/bash root
或者改成正确的
zsh:sudo chsh -s /usr/bin/zsh root

4.2 shell的父子关系

登陆系统时所启动的默认的交互shell,是一个父shell。在CLI提示符后输入/bin/bash命令或者其他等效的命令时,会创建一个新的shell程序,即子shell。子shell也拥有CLI提示符,同样会等待命令输入。

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

当输入bash、生成子shell的时候,是看不到任何相关的信息的,需要ps命令和参数-f配合来查看。

[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root     11910 11906  0 22:30 pts/0    00:00:00 -bash
root     23890 11910 99 23:00 pts/0    00:00:01 ps -f
[root@localhost ~]# bash 
[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root     11910 11906  0 22:30 pts/0    00:00:00 -bash
root     23891 11910  3 23:00 pts/0    00:00:00 bash
root     23900 23891  0 23:00 pts/0    00:00:00 ps -f
[root@localhost ~]# 

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

在生成子shell进程时,只有部分父进程的环境被复制到子shell环境中。子shell(child shell,也叫subshell)可以从父shell中创建,也可以从另一个子shell中创建。

[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root     11910 11906  0 22:30 pts/0    00:00:00 -bash
root     23891 11910  0 23:00 pts/0    00:00:00 bash
root     23917 23891 99 23:11 pts/0    00:00:01 ps -f
[root@localhost ~]#  bash 
[root@localhost ~]# bash
[root@localhost ~]# bash
[root@localhost ~]# ps --forest
  PID TTY          TIME CMD
11910 pts/0    00:00:00 bash
23891 pts/0    00:00:00  \_ bash
23918 pts/0    00:00:00      \_ bash
23927 pts/0    00:00:00          \_ bash
23936 pts/0    00:00:00              \_ bash
23945 pts/0    00:00:00                  \_ ps
[root@localhost ~]# 

在上面的例子中,bash命令被输入了三次。这实际上创建了三个子shell。ps -forest命令展示了这些子shell间的嵌套结构。

linux shell 4. 理解shell_第1张图片
4.2 子shell的嵌套关系.png
[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root     11910 11906  0 22:30 pts/0    00:00:00 -bash
root     23891 11910  0 23:00 pts/0    00:00:00 bash
root     23918 23891  0 23:11 pts/0    00:00:00 bash
root     23927 23918  0 23:11 pts/0    00:00:00 bash
root     23936 23927  0 23:11 pts/0    00:00:00 bash
root     23948 23936  0 23:15 pts/0    00:00:00 ps -f
linux shell 4. 理解shell_第2张图片
4.2-2 bash命令行参数.png

使用exit命令可以退出子shell:

[root@localhost ~]# exit
exit
[root@localhost ~]# exit
exit
[root@localhost ~]# exit
exit
[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root     11910 11906  0 22:30 pts/0    00:00:00 -bash
root     23969 11910  0 23:18 pts/0    00:00:00 ps -f
[root@localhost ~]# ps --forest
  PID TTY          TIME CMD
11910 pts/0    00:00:00 bash
23970 pts/0    00:00:00  \_ ps

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

5.2.1 进程列表

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

$ pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
/etc
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
[root@localhost ~]# ps  -f
UID        PID  PPID  C STIME TTY          TIME CMD
root     11910 11906  0 22:30 pts/0    00:00:00 -bash
root     23984 11910  0 23:26 pts/0    00:00:00 ps -f

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

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

借助环境变量echo $BASH_SUBSHELLk,可以查看是否生成子shell,如果该命令返回0,表名没有子shell,如果返回1或者更大的数字,就表明存在子shell。

[root@localhost ~]# pwd; ls; cd /etc; cd ;pwd; ls;echo $BASH_SUBSHELL
/root
anaconda-ks.cfg
/root
anaconda-ks.cfg
0
[root@localhost ~]# (pwd; ls; cd /etc; cd ;pwd; ls;echo $BASH_SUBSHELL)
/root
anaconda-ks.cfg
/root
anaconda-ks.cfg
1

上例最后输出的数字为1,表明的确创建子shell。

[mycms5@localhost ~]$ (pwd ; echo $BASH_SUBSHELL)
/home/mycms5
1
[mycms5@localhost ~]$ (pwd ; (echo $BASH_SUBSHELL))
/home/mycms5
2

上例返回的数字2,是因为echo $BASH_SUBSHELL外面又多出了一对括号,这对括号在子shell中产生了另一个子shell来执行命令。

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

5.2.2 子shell的用法

    1. 后台模式
      sleep命令接受一个参数,希望进程等待(睡眠)的秒数。这个命令在脚本中常用于引入一段时间的暂停。命令sleep 10会将会话暂停10秒仲,然后返回shell CLI提示符。
[root@localhost ~]# sleep 10
# 注意 这里停顿10秒才会返回下行的提示符
[root@localhost ~]# 

在sleep命令末尾加上字符&,可以将sleep命令置入后台模式。

[root@localhost ~]# sleep 3000&
[1] 1200
[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      1181  1177  0 03:10 pts/0    00:00:00 -bash
root      1200  1181  0 03:13 pts/0    00:00:00 sleep 3000
root      1201  1181  0 03:13 pts/0    00:00:00 ps -f

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

[root@localhost ~]# sleep 3000&
[1] 1200
[root@localhost ~]# sleep 30&
[2] 1225
[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      1181  1177  0 03:10 pts/0    00:00:00 -bash
root      1200  1181  0 03:13 pts/0    00:00:00 sleep 3000
root      1225  1181  0 03:16 pts/0    00:00:00 sleep 30
root      1226  1181  0 03:16 pts/0    00:00:00 ps -f
[root@localhost ~]# 

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

[root@localhost mycms5]# sleep 10&
[1] 1443
[root@localhost mycms5]# jobs
[1]+  Running                 sleep 10 &
[root@localhost mycms5]# jobs -l # 使用-l选项还能够显示出命令的PID
[1]+  1443 Running                 sleep 10 &
[root@localhost mycms5]# jobs
[1]+  Done                    sleep 10  # 后台作业完成,就会显示出结束状态。
    1. 将进程列表置入后台
[root@localhost ~]# (sleep 5; echo $BASH_SUBSHELL; sleep 3)
1
[root@localhost ~]#

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

[root@localhost ~]# (sleep 2; echo $BASH_SUBSHELL; sleep 2)&
[1] 1242
[root@localhost ~]# 1

[1]+  Done                    ( sleep 2; echo $BASH_SUBSHELL; sleep 2 )
[root@localhost ~]# 

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

将进程列表置入后台模式,既可以在子shell中进行繁重的处理工作,也不会让子shell的I/O受制于终端。使用tar命令创建备份文件是有效利用后台进程列表的一个更实用的例子。

[root@localhost ~]# ( tar -cPf /root/lnmp-root.tar /root/lnmp ; sleep 3 ; 
                 tar -cPf /home/rich/lnmp-rich.tar /home/rich/lnmp ; sleep 3;
  tar -cPf /home/mycms5/lnmp-mycms5.tar /home/mycms5/lnmp ; sleep 3)&
[1] 1795
[root@localhost ~]# ls

注意:若提示“tar: Removing leading `/’ from member names”的错误,可以使用-P参数(注意大写)解决这个问题。tar默认为相对路径,使用绝对路径的话就回报这个错。

    1. 协程
      协程可以同时做两件事,它在后台生成一个子shell,并在这个子shell中执行命令。要进行协程处理,得使用coproc命令,还有要在子shell中执行的命令。
[root@localhost ~]# coproc sleep 20
[1] 1829
[root@localhost ~]# coproc My_job { sleep 30;}
[1] 1834
[root@localhost ~]# jobs
[2]   Running                 coproc My_job { sleep 30; } &
[1]-  Running                 coproc COPROC sleep 20 &

通过使用扩展语法,协程的名字被设置成My_Job。这里要注意的是,扩展语法写起来有点麻烦。必须确保在第一个花括号({)和命令名之间有一个空格。还必须保证命令以分号(;)结尾。另外,分号和闭花括号(})之间也得有一个空格。

4.3 理解shell的内建命令

4.3.1 外部命令

外部命令也称文件系统命令,是存在于bash shell之外的程序,并不是shell程序的命令。外部命令通常位于/bin、/usr/bin、/sbin和/usr/sbin中。
ps就是一个外部命令,可以使用which、type命令找到它:

[root@localhost ~]# which ps
/usr/bin/ps
[root@localhost ~]# type -a ps
ps is /usr/bin/ps
[root@localhost ~]# ls -l /bin/ps
-rwxr-xr-x. 1 root root 100120 Aug  4 07:12 /bin/ps

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

[root@localhost ~]# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      1617  1613  0 02:52 pts/0    00:00:00 -bash
root      1909  1617  0 03:02 pts/0    00:00:00 ps -f

作为外部命令,ps命令执行时会创建出一个子进程。在这里,ps命令的PID是1909,父PID是1617。作为父进程的bash shell的PID是1617。

4.3.2 内建命令

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

[root@localhost ~]# type cd 
cd is a shell builtin
[root@localhost ~]# type exit
exit is a shell builtin

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

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

[root@localhost ~]# type -a echo
echo is a shell builtin
echo is /usr/bin/echo
[root@localhost ~]# type -a echo 
echo is a shell builtin
echo is /usr/bin/echo
[root@localhost ~]# which echo 
/usr/bin/echo
[root@localhost ~]# type -a pwd
pwd is a shell builtin
pwd is /usr/bin/pwd
[root@localhost ~]# which pwd
/usr/bin/pwd

命令type -a显示出了每个命令的两种实现。注意,which命令只显示出了外部命令文件。对于有多种实现的命令,如果想要使用其外部命令实现,直接指明对应的文件就可以了。例如,要使用外部命令pwd,可以输入/bin/pwd。

    1. history命令
      history命令是一个内建命令,通常历史记录中会保存最近的1000条命令。bash shell会跟踪你用过的命令,输入!!,然后按回车键可以唤回刚刚用过的命令并重新使用。
      命令历史记录被保存在隐藏文件.bash_history中,它位于用户的主目录中。这里要注意的是,bash命令的历史记录是先存放在内存中,当shell退出时才被写入到历史文件中。
      使用cat .bash_history可以查看命令历史记录,若想在退出shell之前强制将命令历史记录写入.bash_history文件,命令为history -a。
$ history -a
$
$ history
[...]
25 ps --forest
26 history
27 ps --forest
28 history
29 ls -a
30 cat .bash_history
31 history -a
32 history
$
$ cat .bash_history
[...]
ps --forest
history
ps --forest
history
ls -a
cat .bash_history
history -a

上文输出内容过长,做了删减。

    1. 命令别名
      alias命令是另一个shell的内建命令。命令别名允许你为常用的命令(及其参数)创建另一个名称,从而将输入量减少到最低。
      你所使用的Linux发行版很有可能已经为你设置好了一些常用命令的别名。要查看当前可用
      的别名,使用alias命令以及选项-p。
[root@localhost ~]# alias -p
alias cp='cp -i'
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 mv='mv -i'
alias rm='rm -i'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
[root@localhost ~]# 

注意,在该Ubuntu Linux发行版中,有一个别名取代了标准命令ls。它自动加入了--color选项,表明终端支持彩色模式的列表。当然,可以使用alias命令创建属于自己的别名。

[root@localhost ~]# alias li='ls -li'
[root@localhost ~]# li
total 54848
100663362 -rw-------. 1 root root      1265 Oct  7 06:13 anaconda-ks.cfg
 33559059 drwxr-xr-x. 7 root root       228 Jun 18  2015 lnmp
100663380 -rw-r--r--. 1 root root    552960 Oct  7 04:58 lnmp-root.tar
100663376 -rw-r--r--. 1 root root    552960 Oct  7 04:57 lnmp.tar
 34375845 drwxr-xr-x. 5  501 games      238 Oct  7 13:55 pip-9.0.1
101299173 -rw-r--r--. 1 root root   1197370 Nov  6  2016 pip-9.0.1.tar.gz
100986064 -rw-r--r--. 1 root root  25030571 Mar 14  2015 webmin-1.740-1.noarch.rpm
101218533 -rw-r--r--. 1 root root  28823484 Jun 25 18:35 webmin-1.850-1.noarch.rpm
[root@localhost ~]# 

在定义好别名之后,你随时都可以在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的代价可不低,因为还必须为子shell创建出一个全新的环境。

内建命令和外部命令。外部命令会创建出一个包含全新环境的子进程,而内建命令则不会。相比之下,外部命令的使用成本更高。内建命令因为不需要创建新环境,所以更高效,不会受到环境变化的影响。

你可能感兴趣的:(linux shell 4. 理解shell)