title: Shell(3)shell高级

date: 2021-06-04 20:29:08.113
updated: 2021-06-04 22:36:09.159
url: /?p=199
categories: Linux
tags: Linux

输入输出重定向

Linux Shell 重定向分为两种,一种输入重定向,一种是输出重定向;

输入还是输出的“参考物”,是程序的内存。

输入输出方向就是数据的流动方向:

  • 输入方向就是数据从哪里流向程序。数据默认从键盘流向程序,如果改变了它的方向,数据就从其它地方流入,这就是输入重定向。
  • 输出方向就是数据从程序流向哪里。数据默认从程序流向显示器,如果改变了它的方向,数据就流向其它地方,这就是输出重定向。

输出重定向

输出重定向是指命令的结果不再输出到显示器上,而是输出到其它地方,一般是文件中。这样做的最大好处就是把命令的结果保存起来,当我们需要的时候可以随时查询。Bash 支持的输出重定向符号如下表所示。

类 型 符 号 作 用
标准输出重定向 command >file 以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。
command >>file 以追加的方式,把 command 的正确输出结果输出到 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 不推荐】这两种写法会导致 file 被打开两次,引起资源竞争,所以 stdout 和 stderr 会互相覆盖,我们将在《结合Linux文件描述符谈重定向,彻底理解重定向的本质
》一节中深入剖析。
command >>file 2>>file

在输出重定向中,>代表的是覆盖,>>代表的是追加。

输入重定向

输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而是使用文件作为命令的输入。

符号 说明
command <file 将 file 文件中的内容作为 command 的输入。
command <<END 从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止(分界符可以是任意的字符串,用户自己定义)。
command file2 将 file1 作为 command 的输入,并将 command 的处理结果输出到 file2。

和输出重定向类似,输入重定向的完整写法是fd<file,其中 fd 表示文件描述符,如果不写,默认为 0,也就是标准输入文件。

文件描述符

一个 Linux 进程可以打开成百上千个文件,为了表示和区分已经打开的文件,Linux 会给每个文件分配一个编号(一个 ID),这个编号就是一个整数,被称为文件描述符(File Descriptor)。

一个 Linux 进程启动后,会在内核空间中创建一个 PCB (进程控制块),PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。

除了文件描述符表,系统还需要维护另外两张表:

  • 打开文件表(Open file table)
  • i-node 表(i-node table)

文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个,它们三者之间的关系如下图所示。

从本质上讲,这三种表都是结构体数组,0、1、2、73、1976 等都是数组下标。表头只是我自己添加的注释,数组本身是没有的。实线箭头表示指针的指向,虚线箭头是我自己添加的注释。

emmm,文件描述符只不过是一个数组下标吗!

通过文件描述符,可以找到文件指针,从而进入打开文件表。该表存储了以下信息:

  • 文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
  • 状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
  • 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 这三个特殊的文件);
  • 不同进程的不同文件描述符也可以指向同一个文件。

重定向的本质

Linux每次读写文件都从文件描述符下手通过文件描述符找到文件指针,然后进入打开文件表和 i-node 表,这两个表里面才真正保存了与打开文件相关的各种信息。

试想一下,如果我们改变了文件指针的指向,不就改变了文件描述符对应的真实文件吗?比如文件描述符 1 本来对应显示器,但是我们偷偷将文件指针指向了 log.txt 文件,那么文件描述符 1 也就和 log.txt 对应起来了。

文件指针只不过是一个内存地址,修改它是轻而易举的事情。文件指针是文件描述符和真实文件之间最关键的“纽带”,然而这条纽带却非常脆弱,很容易被修改。

Linux 系统提供的函数可以修改文件指针,比如 dup()、dup2();Shell 也能修改文件指针,输入输出重定向就是这么干的。

更准确地说,发生重定向时,Linux 会用文件描述符表(一个结构体数组)中的一个元素给另一个元素赋值,或者用一个结构体变量给数组元素赋值,整体上的资源开销相当低。

以下面的语句为例来说明:

1
echo "c.biancheng.net" 1>log.txt

文件描述符表本质上是一个结构体数组,假设这个结构体的名字叫做 FD。发生重定向时,Linux 系统首先会打开 log.txt 文件,并把各种信息添加到 i-node 表和文件打开表,然后再创建一个 FD 变量(通过这个变量其实就能读写文件了),并用这个变量给下标为 1 的数组元素赋值,覆盖原来的内容,这样就改变了文件指针的指向,完成了重定向。

前面提到,>是输出重定向符号,<是输入重定向符号;更准确地说,它们应该叫做文件描述符操作符。> 和 < 通过修改文件描述符改变了文件指针的指向,所以能够实现重定向的功能。

除了 > 和 <,Shell 还是支持<>,它的效果是前面两者的总和。

分类 用法 说明
输出 n>filename 以输出的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 1,也即标准输出文件。
n>&m 用文件描述符 m 修改文件描述符 n,或者说用文件描述符 m 的内容覆盖文件描述符 n,结果就是 n 和 m 都代表了同一个文件,因为 n 和 m 的文件指针都指向了同一个文件。
            因为使用的是`>`

,所以 n 和 m 只能用作命令的输出文件。n 可以不写,默认为 1。 |
| | n>&- | 关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 1。 |
| | &>filename | 将正确输出结果和错误信息全部重定向到 filename。 |
| 输入 | n<filename | 以输入的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 0,也即标准输入文件。 |
| | n<&m | 类似于 n>&m,但是因为使用的是<
,所以 n 和 m 只能用作命令的输入文件。n 可以不写,默认为 0。 |
| | n<&- | 关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 0。 |
| 输入和输出 | n<>filename | 同时以输入和输出的方式打开文件 filename,并绑定到文件描述符 n,相当于 n>filename 和 n<filename 的总和。。n 可以不写,默认为 0。 |

使用exec命令操作文件描述符

代码块重定向(对一组命令进行重定向)

Here Document(内嵌文档/立即文档

Here String(内嵌字符串,嵌入式字符串)

组命令(把多条命令看做一个整体)

进程替换(把一个命令的输出传递给另一个命令)

Shell管道

我们已经学过从文件重定向输入,以及重定向输出到文件。Shell 还有一种功能,就是可以将两个或者多个命令(程序或者进程)连接到一起,把一个命令的输出作为下一个命令的输入,以这种方式连接的两个或者多个命令就形成了管道(pipe)

Linux 管道使用竖线|连接多个命令,这被称为管道符。Linux 管道的具体语法格式如下:

1
2
command1 | command2
command1 | command2 [ | commandN... ]

当在两个命令之间设置管道时,管道符|左边命令的输出就变成了右边命令的输入。只要第一个命令向标准输出写入,而第二个命令是从标准输入读取,那么这两个命令就可以形成一个管道。大部分的 Linux 命令都可以用来形成管道。

这里需要注意,command1 必须有正确输出,而 command2 必须可以处理 command2 的输出结果;而且 command2 只能处理 command1 的正确输出结果,不能处理 command1 的错误信息。

重定向和管道的区别

乍看起来,管道也有重定向的作用,它也改变了数据输入输出的方向,那么,管道和重定向之间到底有什么不同呢?

简单地说,重定向操作符>将命令与文件连接起来,用文件来接收命令的输出;而管道符|将命令与命令连接起来,用第二个命令来接收第一个命令的输出。如下所示:

1
2
command > file
command1 | command1

有些读者在学习管道时会尝试如下的命令,我们来看一下会发生什么:

1
command1 > command2

答案是,有时尝试的结果将会很糟糕。这是一个实际的例子,一个 Linux 系统管理员以超级用户(root 用户)的身份执行了如下命令:

1
2
cd /usr/bin
ls > less

第一条命令将当前目录切换到了大多数程序所存放的目录,第二条命令是告诉 Shell 用 ls 命令的输出重写文件 less。因为 /usr/bin 目录已经包含了名称为 less(less 程序)的文件,第二条命令用 ls 输出的文本重写了 less 程序,因此破坏了文件系统中的 less 程序。

这是使用重定向操作符错误重写文件的一个教训,所以在使用它时要谨慎。

Linux管道实例

【实例1】将 ls 命令的输出发送到 grep 命令:

1
2
~$ ls | grep log.txt
log.txt

上述命令是查看文件 log.txt 是否存在于当前目录下。

我们可以在命令的后面使用选项,例如使用-al选项:

1
2
~$ ls -al | grep log.txt
-rw-rw-r--. 1 root root 0 4月 15 17:26 log.txt

管道符|与两侧的命令之间也可以不存在空格,例如将上述命令写作ls -al|grep log.txt;然而我还是推荐在管道符|和两侧的命令之间使用空格,以增加代码的可读性。

我们也可以重定向管道的输出到一个文件,比如将上述管道命令的输出结果发送到文件 output.txt 中:

1
2
3
~$ ls -al | grep log.txt >output.txt
~$ cat output.txt
-rw-rw-r--. 1 root root 0 4月 15 17:26 log.txt

【实例2】使用管道将 cat 命令的输出作为 less 命令的输入,这样就可以将 cat 命令的输出每次按照一个屏幕的长度显示,这对于查看长度大于一个屏幕的文件内容很有帮助。
cat /var/log/message | less

【实例3】查看指定程序的进程运行状态,并将输出重定向到文件中。
~$ ps aux | grep httpd > /tmp/ps.output
~$ cat /tem/ps.output
mozhiyan  4101     13776  0   10:11 pts/3  00:00:00 grep httpd
root      4578     1      0   Dec09 ?      00:00:00 /usr/sbin/httpd
apache    19984    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19985    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19986    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19987    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19988    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19989    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19990    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd
apache    19991    4578   0   Dec29 ?      00:00:00 /usr/sbin/httpd

【实例4】显示按用户名排序后的当前登录系统的用户的信息。
[c.biancheng.net]$ who | sort
mozhiyan :0           2019-04-16 12:55 (:0)
mozhiyan pts/0        2019-04-16 13:16 (:0)
who 命令的输出将作为 sort 命令的输入,所以这两个命令通过管道连接后会显示按照用户名排序的已登录用户的信息。

【实例5】统计系统中当前登录的用户数。
[c.biancheng.net]$ who | wc -l
5

Shell过滤器

子Shell和子进程到底有什么区别?

如何检测子Shell和子进程?

Linux中的信号是什么?

Bash Shell中的信号简述

Linux进程简明教程

Linux使用什么命令查看进程

Shell向进程发送信号(kill、pkill和killall命令)

Shell trap命令:捕获信号

trap命令捕获信号实例演示

Shell移除(重置)信号捕获

关于Linux Shell中进程、信号和捕获的总结

Shell模块化(把代码分散到多个脚本文件中)

参考

  1. http://c.biancheng.net/shell/advanced/