linux-从设备文件看重定向

linux-从设备文件看重定向

我们在《linux-特殊设备文件》一文中介绍了标准输入输出(错误)设备和终端设备,指出默认情况下输入的信息来自标准输入设备(stdin),程序返回的一般信息交给标准输出设备(stdout),返回的错误信息交给标准错误设备(stderr),而标准输入输出(错误)设备在默认情况下,都会被链接到正在被操作的虚拟终端(tty)或伪终端(pty),最终转发到我们的交互界面。

从数据流的角度来看,正常情况下,输入数据流是Linux内核将键盘输入的数据接收后,再写入/dev/stdin设备,/dev/stdin设备再将数据传递给相应的程序;输出数据流是程序将返回的信息写入/dev/stdout,/dev/stderr设备,Linux内核从/dev/stdout,/dev/stderr设备读取程序的信息,并最终交付给物理终端(显示器)。如下图:

标准输入输出简化图

从结果上来讲,上述流程大体是对的,如果只是正常使用Linux系统,理解到此也没什么问题。如果我们深入地了解它们之间的关系,会发现以上的描述存在一些不准确的地方。

终端设备文件

当我们使用基于命令行的用户接口,CLI Shell,时(例如Bash,sh,zsh),都是通过终端模拟器和Linux系统进行交互。如果这个终端模拟器使用的是内核中实现的终端模拟器,那么叫做虚拟终端,为用户提供了/dev/ttyX的设备接口文件;若这个终端模拟器工作在Linux用户区甚至远程客户端(如ssh),那么叫做伪终端,为用户提供了/dev/ptmx/dev/pts/X这两个设备接口文件。对用户区程序而言,无论是/dev/ttyX还是/dev/pts/XI/O操作都表现得像个标准终端。

注:Linux诞生之初,用户通过物理终端和Linux计算机连接交互,后来物理终端被淘汰变成键盘、显示器等外设,然后Linux内部机制却已经和物理终端模式深深绑定且工作良好,因此Linux系统为了保持兼容性(少改代码),就在系统中用软件实现了一个模仿终端工作的终端模拟器,来“欺骗”其他模块:我们仍然在和终端交互,什么都不用改~~终端发展历程可参考文章《linux-与终端交互的发展》。

我们可以在/dev下看到这些虚拟终端或伪终端设备的设备文件描述符(file descriptor, fd)。

 1$ ls -al /dev/tty[0-9] /dev/pts/*
 2crw--w---- 1 lelouch tty  136, 0 Dec 29 23:39 /dev/pts/0
 3c--------- 1 root    root   5, 2 Oct  8 14:47 /dev/pts/ptmx
 4crw--w---- 1 root    tty    4, 0 Oct  8 14:47 /dev/tty0
 5crw--w---- 1 root    tty    4, 1 Oct  8 14:47 /dev/tty1
 6crw--w---- 1 root    tty    4, 2 Oct  8 14:47 /dev/tty2
 7crw--w---- 1 root    tty    4, 3 Oct  8 14:47 /dev/tty3
 8crw--w---- 1 root    tty    4, 4 Oct  8 14:47 /dev/tty4
 9crw--w---- 1 root    tty    4, 5 Oct  8 14:47 /dev/tty5
10crw--w---- 1 root    tty    4, 6 Oct  8 14:47 /dev/tty6
11crw--w---- 1 root    tty    4, 7 Oct  8 14:47 /dev/tty7
12crw--w---- 1 root    tty    4, 8 Oct  8 14:47 /dev/tty8
13crw--w---- 1 root    tty    4, 9 Oct  8 14:47 /dev/tty9

对某一CLI shell以及派生的子进程而言,相对应的那个/dev/ttyX/dev/pts/X才是与其直接交互的“终端设备”。我们可以用tty指令,查看当前正在使用哪个终端设备。

1$ tty
2/dev/pts/0

那这个终端设备和标准输入输出有什么关系呢?

标准输入输出设备与终端设备文件

为了查看标准输入输出设备的真面目,我们来查看当前CLI shell的标准输入输出端口:

1$ ls -l /dev/std*
2lrwxrwxrwx 1 root root 15 Oct  8 22:47 /dev/stderr -> /proc/self/fd/2
3lrwxrwxrwx 1 root root 15 Oct  8 22:47 /dev/stdin -> /proc/self/fd/0
4lrwxrwxrwx 1 root root 15 Oct  8 22:47 /dev/stdout -> /proc/self/fd/1

从显示出来的详细性质,我们发现输入输出设备对应的不是设备描述符,而是一个链接,链接的位置就是当前进程的/proc/self/fd/0-2/proc/self/是一个动态的目录,它始终指向当前前台进程,而标准输入输出链接到该特殊目录中的文件描述符,则保证了任何在当前在前台工作的进程,都能够和终端直接交互。

当执行一个进程时,都会默认打开3个文件0,1,2,每个文件有对应的文件描述符来方便我们使用:

文件描述符 类型 默认情况 对应文件句柄位置
0 标准输入(standard input) 从键盘获得输入 /proc/self/fd/0
1 标准输出(standard output) 输出到屏幕(即控制台) /proc/self/fd/1
2 错误输出(error output) 输出到屏幕(即控制台) /proc/self/fd/2

好的,接下来我们看看/proc/self/fd/0-2是什么样的文件:

1ls -l /proc/self/fd/
2lrwx------ 1 lelouch lelouch 64 Dec 30 00:31 0 -> /dev/pts/0
3lrwx------ 1 lelouch lelouch 64 Dec 30 00:31 1 -> /dev/pts/0
4lrwx------ 1 lelouch lelouch 64 Dec 30 00:31 2 -> /dev/pts/0

哈,/proc/self/fd/0-2也都只是软链接,链接的位置就是我们使用tty指令显示出来的终端设备文件!也是说,无论是标准输入,还是标准输出、标准错误,最终都是在和终端设备接口文件交互。其关系如下图:

标准输入输出与终端设备

图中黄色部分表示虚拟终端的流程;蓝色部分表示伪终端流程。父进程通过fork和exec派生出不同子进程,通常子进程会继承父进程的终端接口文件,并占据前台(foreground)。从图中,我们可以看出标准输入输出设备(/dev/{stdin,stdout,dtderr})始终是一个动态的链接,始终指向前台进程;而进程产生的三个链接文件/proc/$pid_number/fd/0-2才是进程I/O的核心,是与进程直接交互的接口。如果我们希望进程改变I/O的输入输出方向,更改/dev/{stdin,stdout,stderr}是没有用的,应该更改0,1,2所链接的位置。它们默认链接位置为/dev/ttyX/dev/pts/X,这些设备描述符最终会将数据转发到外设,如键盘、屏幕、网络调制解调器等。

一切皆文件

我们在聊输入输出重定向之前,我们在说说/dev/ttyX/dev/pts/X。之前说过,它们是提供给用户进程的终端设备接口文件,本质上它们是文件啊。那么对于Linux而言,终端设备接口文件与文件系统中的文档有区别吗?答案是:没有,它们都是文件。

linux 中所有内容都是以文件的形式保存和管理的,即一切皆文件,普通文件是文件,目录(Windows 下称为文件夹)是文件,硬件设备(键盘、监视器、硬盘、打印机)是文件,就连套接字(socket)、网络通信等资源也都是文件。

这些内容(无论是普通文件、硬件设备、目录、套接字、链接)需要被操作时,都用**统一的文件描述符(file descriptor, fd)**来表示,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。只要我们有读写权限,我们就可以从文件中读取内容或是向文件写入内容,不管这个文件实际上是设备还是别的什么。当然对某些设备文件或内存文件而言,这种操作相当危险。

因此,当我们进行进程的I/O重定向时,从Linux系统的角度看,只不过是把读取/写入的操作从一个文件转向另一个文件而已

输入输出重定向

由于在Linux中一切皆文件,所谓“重定向”就是换个操作的文件嘛。默认情况下,我们进行I/O的文件是/proc/self/fd/{0,1,2},重定向的时候就是把默认输入文件0换个别的文件,或者默认输出1、2换个别的文件。此时,我们需要用的重定向符号:<,>>,>

简单用法如下:

命令 说明
command > file 将输出从1重定向到 file。命令执行command然后将输出的内容存入file,覆盖原内容。
command < file 将输入从0重定向到 file。需要从键盘获取输入的命令会转移到文件读取内容。
command >> file 将输出从1以追加的方式重定向到file。命令执行command然后将输出的内容追加到file。
command < file1 >file2 command 命令将输入0重定向到 file1,将输出1重定向到 file2。
 1# 输出重定向到文件test_1
 2$ echo "redirection test" > test_1
 3$ cat test_1
 4redirection test
 5# 输出重定向到文件test_1,覆盖原有内容
 6$ echo "redirection test again" > test_1
 7$ cat test_1
 8redirection test again
 9# 输出重定向到文件test_1,追加内容
10$ echo "redirection test again" >> test_1
11$ cat test_1
12redirection test again
13redirection test again
14# 输入重定向,来自test_1
15$ cat < test_1
16redirection test again
17redirection test again
18# 输入重定向,来自test_1,输出重定向到test_11
19$ cat < test_1 >test_11
20$ cat test_11
21redirection test again
22redirection test again
23

实际上,上面这四个命令都是简写,写完整了应该是(数字和重定向符号之间不要空格!):

命令 说明
command 1> file 将输出从1重定向到 file。命令执行command然后将输出的内容存入file,覆盖原内容。
command 0< file 将输入从0重定向到 file。需要从键盘获取输入的命令会转移到文件读取内容。
command 1>> file 将输出从1以追加的方式重定向到file。命令执行command然后将输出的内容追加到file。
command 0< file_1 1>file_2 command 命令将输入0重定向到 file_1,将输出1重定向到 file_2。

相应的,如果我们想要把标准错误2重定向到其他地方,就需要手动地将数字给填上:

命令 说明
command 2> file 将标准错误从2重定向到 file。命令执行command并将出错内容存入file,覆盖原内容。
command 2>> file 将标准错误从2以追加的方式重定向到file。命令执行command并将出错内容追加到file。
1# 错误内容重定向到文件test_2
2$ cat /etc/shadow 2> test_2
3$ cat test_2
4cat: /etc/shadow: Permission denied
5# 错误内容重定向到文件test_2,内容追加
6$ qwertyui 2>> test_2
7$ cat test_2
8cat: /etc/shadow: Permission denied
9-bash: ovcosdjfo: command not found

注意<< Here doucment

Here Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。简单的说,就是运行我们一次性输入很多内容然后一起交给程序执行。

它的基本的形式如下:

1command << delimiter
2    document
3delimiter

它的作用是将两个“delimiter”(定界符)之间的内容(document) 作为输入传递给command。比如

1# 在命令行中通过 wc -l 命令计算 Here Document 的行数:
2$ wc -l << EOF
3    Hello
4    redirction test
5    Here document test
6EOF
73 # 输出结果为 3 行

注意:

  • 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和tab缩进。如果不满足这些条件,则不被认为是delimiter。(下例所示)
  • 开始的delimiter前后的空格会被忽略掉。
  • delimiter具体内容可以自定义,不必非要是“EOF”,你写成“superman”也是可以的,只要开头结尾一致就行。
1# 自定义delimiter
2$ wc -l << superman
3    Hello
4    redirction test
5    Here document test superman
6    superman
7    superman
8superman
95 # 输出结果为 5 行

&符号

Linux中,&符号通常表示将进程放到后台执行,但是在重定向语境下,&后面紧跟一个数字时,表示该进程中,此数字代表的文件描述符。例如&0表示进程的标准输入文件描述符,&1表示进程的标准输出文件描述符,&2表示进程的标准输出错误描述符。其他打开的文件描述符也可以用&+数字表示。

有了这些认识才能理解 "1>&2" 和 "2>&1".

  • 1>&2 标准输出返回值传递给2输出通道 &2表示2号文件描述符,即标准错误通道。如果此处错写成 1>2, 就表示把1输出重定向到名称为“2”的文件中。
  • 2>&1 标准错误返回值传递给1输出通道, 同样&1表示1号文件描述符,即标准输出通道。

再举个例子:

1# 此时,我们目录下并无test.log文件
2$ rm test.log > /dev/null 2>&1 # 写完整了应是 1> /dev/null 2>&1

这个命令先将标准输出重定向到/dev/null,再把错误输出重定向到1输出通道,同样是/dev/null,所以运行这个脚本不会输出任何信息到终端。

/dev/null代表linux的空设备文件,所有往这个文件里面写入的内容都会丢失,俗称“黑洞”。比较常见的用法是把不需要的输出重定向到这个文件。

需要注意的是:如果我们把1> /dev/null 2>&1的顺序调换过来,意思就不一样了。

1# 此时,我们目录下并无test.log文件
2$ rm test.log 2>&1 >/dev/null #写完整了应是 2>&1 1>/dev/null
3rm: cannot remove 'test.log': No such file or directory

我们发现依然可以看到输出的标准错误信息。这是因为第一步2>&1的时候,&1指向的还是/dev/pts/X,也就是说标准错误先是被重定向到了/dev/pts/X(和原来一样);当第二步1>/dev/null的时候,仅有标准输出被重定向到了/dev/null,标准错误仍然指向/dev/pts/X。所以,错误信息还是会显示在屏幕上。

&符号的另一个用法是和重定向符号组合到一起同时代表输出和标准错误输出(&>,&>>)。比如,&>文件名表示将标准输出和标准错误全部保存到指定文件中,等同于1>文件名 2>文件名1>文件名 2>&1。同样,&>>文件名可表示追加写入文件。

参考内容

https://www.runoob.com/linux/linux-shell-io-redirections.html