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间的嵌套结构。
[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
使用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的用法
-
- 后台模式
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 # 后台作业完成,就会显示出结束状态。
-
- 将进程列表置入后台
[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默认为相对路径,使用绝对路径的话就回报这个错。
-
- 协程
协程可以同时做两件事,它在后台生成一个子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。
-
- history命令
history命令是一个内建命令,通常历史记录中会保存最近的1000条命令。bash shell会跟踪你用过的命令,输入!!,然后按回车键可以唤回刚刚用过的命令并重新使用。
命令历史记录被保存在隐藏文件.bash_history中,它位于用户的主目录中。这里要注意的是,bash命令的历史记录是先存放在内存中,当shell退出时才被写入到历史文件中。
使用cat .bash_history可以查看命令历史记录,若想在退出shell之前强制将命令历史记录写入.bash_history文件,命令为history -a。
- history命令
$ 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
上文输出内容过长,做了删减。
-
- 命令别名
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创建出一个全新的环境。
内建命令和外部命令。外部命令会创建出一个包含全新环境的子进程,而内建命令则不会。相比之下,外部命令的使用成本更高。内建命令因为不需要创建新环境,所以更高效,不会受到环境变化的影响。