理解 shell

Shell 的类型

用户登录某个虚拟控制台终端或在GUI中运行终端仿真器时,会启动用户对应的 shell 程序。 在 /etc/passwd 文件中,用户记录的第 7 个字段指定了用户默认的 shell. 比如下面 root 用户使用 GNU bash shell 作为自己的默认shell 程序:

$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash

bash shell 程序位于 /bin 目录内,是一个可执行文件:

$ ls -lF /bin/bash
-rwxr-xr-x 1 root root 1216928 Apr 18  2019 /bin/bash*

/bin 内还有其他 shell 程序:

$ ls -lF /bin/*sh
-rwxr-xr-x 1 root root 1216928 Apr 18  2019 /bin/bash*
-rwxr-xr-x 1 root root  129536 Jan 18  2019 /bin/dash*
lrwxrwxrwx 1 root root       4 Apr 18  2019 /bin/rbash -> bash*
lrwxrwxrwx 1 root root       4 Jan  2 20:22 /bin/sh -> dash*

当用户没登陆时,系统也会执行一些脚本,这时系统的默认 shell 是 /bin/sh,从上面的输出中可以看出,/bin/sh 软链接到了 /bin/dash,因为我使用的是 Debian,这是 Debian 的默认 shell.

我们可以改变当前使用的 shell,比如可以直接输入 /bin/dash 来启动 dash shell:

$ /bin/dash
$

那我们怎么知道当前用的是什么 shell 呢?一个简单的方法就是输入一条错误命令:

$ hello
/bin/dash: 4: hello: not found

shell 的父子关系

登录到某个终端时的 shell 是父shell,当输入 /bin/bash 或其他 bash 命令时(就是上面提的命令),会创建一个新的 shell,称为子shell.

$ bash
$ ps --forest
  PID TTY          TIME CMD
20246 pts/3    00:00:00 bash
 5862 pts/3    00:00:00  \_ bash
 8389 pts/3    00:00:00      \_ ps
$ exit
$ ps --forest
  PID TTY          TIME CMD
20246 pts/3    00:00:00 bash
11334 pts/3    00:00:00  \_ ps

创建子 shell 时,部分父 shell 的环境被复制到子 shell 环境中。

另外,运行 shell 脚本也会创建子 shell.

进程列表

要想在一行中依次执行一系列命令,可以用 命令列表 来实现,只需要用分号隔开命令即可:

$ pwd; ls; cd /etc; ls; cd

如果将上述命令用括号括起来,就是 进程列表。进程列表会生成一个子 shell 来执行对应的命令:

$ (pwd; ls; cd /etc; ls; cd)

我们可以通过查看 $BASH_SUBSHELL 的值来判断是否生成了子 shell:

$ echo $BASH_SUBSHELL
0
$ (echo $BASH_SUBSHELL)
1
$ ( (echo $BASH_SUBSHELL) )
2

在父 shell 时,$BASH_SUBSHELL 为 0;每生成一层子 shell,$BASH_SUBSHELL 的值就加 1.

子 shell 搭配后台模式

当我们输入一个命令后,要等到命令执行完毕才能输入下一个命令。如果想在后台执行命令,可以在命令后面加 &

$ sleep 3 #等待3秒 
$ sleep 3&
[1] 8792
$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
pi        8792 20246  0 16:39 pts/3    00:00:00 sleep 3
pi        8799 20246  0 16:39 pts/3    00:00:00 ps -f
pi       20246 20245  0 14:12 pts/3    00:00:00 -bash

当命令置入后台时,会出现两条信息:第一条是方括号中的后台作业号(上面的 [1]),第二条是后台作业的进程 ID(上面的 8792

我们可以用 ps -fjobs 来查看后台作业信息。

$ sleep 3&
[1] 17718
pi@raspbian:~$ jobs -l
[1]+ 17718 Running                 sleep 3 &

利用后台模式,我们可以将进程列表放到后台:

$ (sleep 2; echo $BASH_SUBSHELL; sleep 2)&
[1] 23123
$ 1

我们可以用 协程 命令 coproc 来完成生成子 shell 和执行命令两件事:

$ coproc sleep 10
[1] 24457
$ jobs
[1]+  Running                 coproc COPROC sleep 10 &

COPROC 是 coproc 命令给进程起的名字,用于协程之间通信用。我们也可以自定义名字:

$ coproc My_job { sleep 10; }
[1] 25399

上面我们用到了 扩展语法,即 { sleep 10; },注意命令要用分号结尾,并且与左右花括号之间有一个空格。

内建命令

外部命令,也叫 文件系统命令,是存在于 bash shell 之外的程序,一般位于 /bin、/usr/bin、/sbin、/usr/sbin 中。比如 ps 就是外部命令,我们可以用 whichtype 找到它的位置:

$ which ps
/bin/ps
$ type -a ps
ps is /bin/ps
$ ls -l /bin/ps
-rwxr-xr-x 1 root root 125088 May 31  2018 /bin/ps

外部命令在执行的时候,会先创建一个子进程,称为 衍生 forking

$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
pi        6919 17241  0 19:35 pts/3    00:00:00 ps -f
pi       17241 17240  0 18:35 pts/3    00:00:00 -bash

从上面可以看出,ps 进程的父进程ID(PPID)是 17241,也就是下面那个。

外部命令因为需要进行衍生,所有花费的时间稍多一丢丢。

内建命令则相反,它本身和 shell 编译成了一体,不需要外部程序文件来运行。比如 cdexit,我们可以用 type 来检验:

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

某些命令即有内部实现,也有外部实现,比如:echopwd,默认是使用内部实现,如果要用外部实现,可以手动指明其对应的外部文件。

$ type echo #默认是内建命令
echo is a shell builtin
$ type -a echo #列出不同实现
echo is a shell builtin
echo is /bin/echo
$ which echo
/bin/echo