5.6 Shell 相关 Linux 概念
5.6.1 文件描述符
Linux 中一切皆文件,包括标准输入设备(键盘)和标准输出设备(显示器)在内的所有计算机硬件都是文件。
为了表示和区分已经打开的文件,Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。
表 1:与输入输出有关的文件描述符
文件描述符 | 文件名 | 类型 | 硬件 |
---|---|---|---|
0 | stdin | 标准输入文件 | 键盘 |
1 | stdout | 标准输出文件 | 显示器 |
2 | stderr | 标准错误输出文件 | 显示器 |
Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、 FIFO 、管道、终端、键盘、显示器,甚至是一个网络连接。stdin 、 stdout 、 stderr 默认都是打开的,在重定向的过程中,0 、 1 、 2 这三个文件描述符可以直接使用。
特殊文件标识符
- /dev/null
如果你既不想把命令的输出结果保存到文件,也不想把命令的输出结果显示到屏幕上,干扰命令的执行,那么可以把命令的所有结果重定向到 /dev/null 文件中。 - /dev/tty
文件代表的就是显示器
Linux 文件描述符到底是什么?
一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。
除了文件描述符表,系统还需要维护另外两张表:
- 打开文件表(Open file table)
- i-node 表(i-node table)
文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个,它们三者之间的关系如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gGuZIjEP-1640921183340)(/工具/5%20Shell/img/file-descriptor.gif)]
从本质上讲,这三种表都是结构体数组,0 、 1 、 2 、 73 、 1976 等都是数组下标。表头只是我自己添加的注释,数组本身是没有的。实线箭头表示指针的指向,虚线箭头是我自己添加的注释。
你看,文件描述符只不过是一个数组下标吗!
-
文件描述符
通过文件描述符,可以找到文件指针 -
打开文件表
该表存储了以下信息:- 文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
- 状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
- i-node 表指针。
-
i-node 表
要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表,该表包含了诸如以下的信息:- 文件类型,例如常规文件、套接字或 FIFO。
- 文件大小。
- 时间戳,比如创建时间、更新时间。
- 文件锁。
对上图的进一步说明:
-
在进程 A 中,文件描述符 1 和 20 都指向了同一个打开文件表项,标号为 23(指向了打开文件表中下标为 23 的数组元素),这可能是通过调用 dup()、 dup2()、 fcntl() 或者对同一个文件多次调用了 open() 函数形成的。
-
进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件,这可能是在调用 fork() 后出现的(即进程 A 、 B 是父子进程关系),或者是不同的进程独自去调用 open() 函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
-
进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项,但这些表项均指向 i-node 表的同一个条目(标号为 1976);换言之,它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件,也会发生类似情况。
有了以上对文件描述符的认知,我们很容易理解以下情形:
- 同一个进程的不同文件描述符可以指向同一个文件;
- 不同进程可以拥有相同的文件描述符;
- 不同进程的相同文件描述符可以指向不同的文件(一般也是这样,除了 0 、 1 、 2 这三个特殊的文件);
- 不同进程的不同文件描述符也可以指向同一个文件。
5.6.2 重定向
标准输入输出设备
计算机的硬件设备有很多,常见的输入设备有键盘、鼠标、麦克风、手写板等,输出设备有显示器、投影仪、打印机等。不过,在 Linux 中,
- 标准输入设备 指的是键盘
- 标准输出设备 指的是显示器。
Linux Shell 输出重定向
输出重定向是指命令的结果不再输出到显示器上,而是输出到其它地方,一般是文件中。这样做的最大好处就是把命令的结果保存起来,当我们需要的时候可以随时查询。Bash 支持的输出重定向符号如下表所示。
- 标准输出重定向
符 号 | 作 用 |
---|---|
command >file | 以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。 |
command >>file | 以追加的方式,把 command 的正确输出结果输出到 file 文件中。 |
输出重定向的完整写法其实是 fd>file 或者 fd>>file,其中 fd 表示文件描述符,如果不写,默认为 1,也就是标准输出文件
fd 和>之间不能有空格,否则 Shell 会解析失败;>和 file 之间的空格可有可无。为了保持一致,我习惯在>两边都不加空格。
- 标准错误输出重定向
符 号 | 作 用 |
---|---|
command 2>file | 以覆盖的方式,把 command 的错误信息输出到 file 文件中。 |
command 2>>file | 以追加的方式,把 command 的错误信息输出到 file 文件中。 |
- 正确输出和错误信息同时保存
符 号 | 作 用 |
---|---|
command >file 2>&1 | 以覆盖的方式,把正确输出和错误信息同时保存到同一个文件(file)中。 |
command >>file 2>&1 | 以追加的方式,把正确输出和错误信息同时保存到同一个文件(file)中。 |
command >file1 2>file2 | 以覆盖的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。 |
command >>file1 2>>file2 | 以追加的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。 |
command >file 2>file command >>file 2>>file | 【不推荐】 |
在输出重定向中,>代表的是覆盖,>>代表的是追加。
Linux Shell 输入重定向
输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而是使用文件作为命令的输入。
表 3:Bash 支持的输出重定向符号
符号 | 说明 |
---|---|
command <file | 将 file 文件中的内容作为 command 的输入。 |
command <<END | 从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止(分界符是用户定义的任意的字符串) |
command <file1 >file2 | 将 file1 作为 command 的输入,并将 command 的处理结果输出到 file2。 |
使用 exec 永久重定向机恢复
使用 exec 命令可以将当前进程输入输出永久重定向。
exec >log.txt
将当前 Shell 进程的所有标准输出重定向到 log.txt 文件,它等价于 exec 1>log.txt
。
以输出重定向为例,手动恢复的方法有两种:
- /dev/tty 文件代表的就是显示器,将标准输出重定向到 /dev/tty 即可,也就是 exec >/dev/tty。
- 如果还有别的文件描述符指向了显示器,那么也可以别的文件描述符来恢复标号为 1 的文件描述符,例如 exec >&2。注意,如果文件描述符 2 也被重定向了,那么这种方式就无效了。
代码块重定向
将重定向命令放在代码块的结尾处,就可以对代码块中的所有命令实施重定向。
- 【实例 1 】使用 while 循环不断读取 nums.txt 中的数字,计算它们的总和。
#!/bin/bash
sum=0
while read n; do
((sum += n))
done <nums.txt #输入重定向
echo "sum=$sum"
将代码保存到 test.sh 并运行:
[c.biancheng.net]$ cat nums.txt
80
33
129
71
100
222
8
[c.biancheng.net]$ . ./test.sh
sum=643
对上面的代码进行改进,记录 while 的读取过程,并将输出结果重定向到 log.txt 文件:
#!/bin/bash
sum=0
while read n; do
((sum += n))
echo "this number: $n"
done <nums.txt >log.txt #同时使用输入输出重定向
echo "sum=$sum"
将代码保存到 test.sh 并运行:
[c.biancheng.net]$ . ./test.sh
sum=643
[c.biancheng.net]$ cat log.txt
this number: 80
this number: 33
this number: 129
this number: 71
this number: 100
this number: 222
this number: 8
- 【实例 2 】对{}包围的代码使用重定向。
#!/bin/bash
{
echo "C 语言中文网";
echo "http://www.beylze.com/d/file/20190908/ggmqcykqgwe.net";
echo "7"
} >log.txt #输出重定向
{
read name;
read url;
read age
} <log.txt #输入重定向
echo "$name 已经$age 岁了,它的网址是 $url"
将代码保存到 test.sh 并运行:
[c.biancheng.net]$ . ./test.sh
C 语言中文网已经 7 岁了,它的网址是 http://www.beylze.com/d/file/20190908/ggmqcykqgwe.net
[c.biancheng.net]$ cat log.txt
C 语言中文网
http://www.beylze.com/d/file/20190908/ggmqcykqgwe.net
7
Shell 教程
http://www.beylze.com/d/file/20190908/i5zpldli3og
已经进行了三次改版
Here Document
如果你尝试在脚本嵌入一小块多行数据,使用 Here Document 是很有用的,而嵌入很大的数据块是一个不好的习惯。你应该保持你的逻辑(你的代码)和你的输入(你的数据)分离,最好是在不同的文件中,除非是输入一个很小的数据集。
Here Document 的基本用法为:
command <<END
document
END
- command 是 Shell 命令,
- <<END 是开始标志,END 是结束标志,必须独占一行,并且要定顶格写,分界符(终止符)可以是任意的字符串,由用户自己定义。
- document 是输入的文档(也就是一行一行的字符串)。
示例:
[mozhiyan@localhost ~]$ cat <<END
> shell 教程
> http://www.beylze.com/d/file/20190908/i5zpldli3og
> 已经进行了三次改版
> END
- Here String
Here String 是 Here Document 的一个变种,它的用法如下:
command <<< string
示例:
$ tr a-z A-Z <<< one #将字符串 one 转储大写 ONE
重定向的本质
输入输出重定向就是通过修改文件指针实现的!更准确地说,发生重定向时,Linux 会用文件描述符表(一个结构体数组)中的一个元素给另一个元素赋值,或者用一个结构体变量给数组元素赋值,整体上的资源开销相当低。
以下面的语句为例来说明:
echo "c.biancheng.net" 1>log.txt
文件描述符表本质上是一个结构体数组,假设这个结构体的名字叫做 FD。发生重定向时,Linux 系统首先会打开 log.txt 文件,并把各种信息添加到 i-node 表和文件打开表,然后再创建一个 FD 变量(通过这个变量其实就能读写文件了),并用这个变量给下标为 1 的数组元素赋值,覆盖原来的内容,这样就改变了文件指针的指向,完成了重定向。
5.6.3 进程替换
把一个命令的输出传递给另一个命令
Shell 进程替换有两种写法:
- 一种用来产生标准输出,借助输入重定向,它的输出结果可以作为另一个命令的输入:
<(commands)
例如:
read < <(echo "aaaa")
echo $REPLY
输出结果:
aaaa
两个 < 之间是有空格的,第一个 < 表示输入重定向,第二个 < 和 () 连在一起表示进程替换。
- 另一种用来接受标准输入,借助输出重定向,它可以接收另一个命令的输出结果:
>(commands)
commands 是一组命令列表,多个命令之间以分号 ; 分隔。注意,< 或 > 与圆括号之间是没有空格的。
例如:
echo "C 语言中文网" > >(read; echo "你好,$REPLY")
运行结果:
你好,C 语言中文网
Shell 进程替换的本质
为了能够在不同进程之间传递数据,实际上进程替换会跟系统中的文件关联起来,这个文件的名字为 /dev/fd/n(n 是一个整数)。该文件会作为参数传递给 () 中的命令,() 中的命令对该文件是读取还是写入取决于进程替换格式是 < 还是 >:
- 如果是>(),那么该文件会给()中的命令提供输入;借助输出重定向,要输入的内容可以从其它命令而来。
- 如果是<(),那么该文件会接收()中命令的输出结果;借助输入重定向,可以将该文件的内容作为其它命令的输入。
/dev/fd/目录下有很多序号文件,进程替换一般用的是 63 号文件,该文件是系统内部文件,我们一般查看不到。
5.6.4 管道
shell 还有一种功能,就是可以将两个或者多个命令(程序或者进程)连接到一起,把一个命令的输出作为下一个命令的输入,以这种方式连接的两个或者多个命令就形成了管道(pipe)。
linux 管道使用竖线 | 连接多个命令,这被称为管道符。Linux 管道的具体语法格式如下:
command1 | command2
command1 | command2 [ | commandN... ]
当在两个命令之间设置管道时,管道符|左边命令的输出就变成了右边命令的输入。只要第一个命令向标准输出写入,而第二个命令是从标准输入读取,那么这两个命令就可以形成一个管道。大部分的 Linux 命令都可以用来形成管道。
重定向和管道的区别
重定向操作符 > 将命令与文件连接起来,用文件来接收命令的输出;而管道符 | 将命令与命令连接起来,用第二个命令来接收第一个命令的输出。
使用管道替换 mysqldump 备份的实例:
# 不使用管道
mysqldump -u root -p '123456' wiki > /tmp/wikidb.backup
gzip -9 /tmp/wikidb.backup
scp /tmp/wikidb.backup username@remote_ip:/backup/mysql/
上述这组命令主要做了如下任务:
- mysqldump 命令用于将名为 wike 的数据库备份到文件 /tmp/wikidb.backup;其中-u 和-p 选项分别指出数据库的用户名和密码。
- gzip 命令用于压缩较大的数据库文件以节省磁盘空间;其中-9 表示最慢的压缩速度最好的压缩效果。
- scp 命令(secure copy,安全拷贝)用于将数据库备份文件复制到 IP 地址为 remote_ip 的备份服务器的 /backup/mysql/ 目录下。其中 username 是登录远程服务器的用户名,命令执行后需要输入密码。
使用管道后的命令如下所示:
mysqldump -u root -p '123456' wiki | gzip -9 | ssh username@remote_ip "cat > /backup/wikidb.gz"
过滤器
将几个命令通过管道符组合在一起就形成一个管道。通常,通过这种方式使用的命令就被称为过滤器。过滤器会获取输入,通过某种方式修改其内容,然后将其输出。
简单地说,过滤器可以概括为以下两点:
- 如果一个 linux 命令是从标准输入接收它的输入数据,并在标准输出上产生它的输出数据(结果),那么这个命令就被称为过滤器。
- 过滤器通常与 Linux 管道一起使用。
常用的过滤器命令
常用的被作为过滤器使用的命令如下所示:
命令 | 说明 |
---|---|
awk | 用于文本处理的解释性程序设计语言,通常被作为数据提取和报告的工具。 |
cut | 用于将每个输入文件(如果没有指定文件则为标准输入)的每行的指定部分输出到标准输出。 |
grep | 用于搜索一个或多个文件中匹配指定模式的行。 |
tar | 用于归档文件的应用程序。 |
head | 用于读取文件的开头部分(默认是 10 行)。如果没有指定文件,则从标准输入读取。 |
paste | 用于合并文件的行。 |
sed | 用于过滤和转换文本的流编辑器。 |
sort | 用于对文本文件的行进行排序。 |
split | 用于将文件分割成块。 |
strings | 用于打印文件中可打印的字符串。 |
tac | 与 cat 命令的功能相反,用于倒序地显示文件或连接文件。 |
tail | 用于显示文件的结尾部分。 |
tee | 用于从标准输入读取内容并写入到标准输出和文件。 |
tr | 用于转换或删除字符。 |
uniq | 用于报告或忽略重复的行。 |
wc | 用于打印文件中的总行数、单词数或字节数。 |
接下来,我们通过几个实例来演示一下过滤器的使用。
过滤器实例
查看系统中的所有的账号名称
awk -F: '{print $1}' /etc/passwd | sort
列出当前账号最常使用的 10 个命令。
history | awk '{print $2}' | sort | uniq -c | sort -rn | head
显示当前系统的总内存大小
free | grep Mem | awk '{print $2}'
查看系统中登录 shell 是“/bin/bash”的用户名和对应的用户主目录的信息
grep "bin/bash" /etc/passwd | cut -d: -f1,6
查看当前机器的 CPU 类型
cat /proc/cpuinfo | grep name | cut -d: -f2 | uniq
查看当前目录下的子目录数
ls -l | cut -c 1 | grep d | wc -l
登录 shell 是“/bin/bash”的用户名和对应的用户主目录的信息
grep "bin/bash" /etc/passwd | cut -d: -f1,6
序列表中所有命令名中包含关键字 zip 的命令
ls /bin /usr/bin | sort | uniq | grep zip
5.6.5 进程
进程是运行在 Linux 中的程序的一个实例。每当你在 Linux 中执行一个命令,它都会创建,或启动一个新的进程。
操作系统通过被称为 PID 或进程 ID 的数字编码来追踪进程。系统中的每一个进程都有一个唯一的 PID。
5.6.6 子进程与子 Shell
同样是创建子进程,但是结果却大相径庭:
-
只使用 fork() 函数,子进程和父进程几乎是一模一样的,父进程中的函数、变量、别名等在子进程中仍然有效。
组命令、命令替换、管道这几种语法都使用这种方式创建进程,所以子进程可以使用父进程的一切,包括全局变量、局部变量、别名等。我们将这种子进程称为子 Shell(sub shell)。
子 Shell 虽然能使用父 Shell 的的一切,但是如果子 Shell 对数据做了修改,比如修改了全局变量,那么这种修改只能停留在子 Shell,无法传递给父 Shell。 -
使用 fork() 和 exec() 函数,子进程和父进程之间除了硬生生地维持一种 父子关系 外,再也没有任何联系了,它们就是两个完全不同的程序。
对于 Shell 来说,以新进程的方式运行脚本文件,比如 bash ./test.sh 、 chmod +x ./test.sh; ./test.sh,或者在当前 Shell 中使用 bash 命令启动新的 Shell。
5.6.7 信号
在 Linux 系统(以及其他类 Unix 操作系统)中,信号被用于进程间的通信。信号是一个发送到某个进程或同一进程中的特定线程的异步通知,用于通知发生的一个事件。
当进程收到一个信号时,可能会发生以下 3 种情况:
- 进程可能会忽略此信号。有些信号不能被忽略,而有些没有默认行为的信号,默认会被忽略。
- 进程可能会捕获此信号,并执行一个被称为信号处理器的特殊函数。
- 进程可能会执行信号的默认行为。例如,信号 15(SIGTERM) 的默认行为是结束进程。
捕获信号
命令用于在接收到指定信号后要执行的动作,通常用途是在 shell 脚本被中断时完成清理工作。trap 的命令语法:
trap command signal
参数说明:
- command 可以是 linux 命令,或用户定义的函数。
- signal 是信号名称或信号数,可以指定多个信号,以空格相隔。
示例:
trap "ehco 'program exit...'; exit 2" SIGINT #脚本在执行时按下 CTRL+c 时,将显示"program exit..."并退出
trap '' 2 3 8 # 脚本运行时忽略 SIGINT SIGQUIT SIGFPE 等信号
哪些情况会引发信号
- 键盘事件 ctrl +c、 ctrl +\
- 非法内存 如果内存管理出错,系统就会发送一个信号进行处理
- 硬件故障 同样的,硬件出现故障系统也会产生一个信号
- 环境切换 比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统
怎样查看信号呢
kill -l
常见信号
信号量值 | 名称 | 发送方式 | 说明 |
---|---|---|---|
(2) | SIGINT | ctrl +c | 终止信号 |
(3) | SIGQUIT | ctrl + \ | 暂停信号,放入后台 |
(4) | SIGILL | 非法指令 | |
(5) | SIGTRAP | abort(3) | 进程异常终止 |
(9) | SIGKILL | kill - 9 pid | 杀死进程 |
(11) | SIGSEGV | 段错误 | |
(13) | SIGPIPE | 管道破裂 | |
(14) | SIGALRM | 闹钟 | |
(15) | SIGTERM | 缺省终止某个进程,终止掉 | |
(17) | SIGSTOP | 子进程死的时候会给父进程发送这个信号 | |
(19) | SIGCONT | 进程暂停 | |
(23) | SIGURG | 紧急数据 | |
(29) | SIGINFO | 异步 IO |