首页 > 技术文章 > shell 重定向以及文件描述符

mianbaoshu 2020-11-20 14:37 原文

1.对重定向的理解

  • Linux Shell 重定向分为两种,一种输入重定向,一种是输出重定向;从字面上理解,输入输出重定向就是「改变输入与输出的方向」的意思。
  • 输入方向就是数据从哪里流向程序。标准输入方向是指数据从键盘流向程序,如果改变了它的方向,数据就从其它地方流入,这就是输入重定向。
  • 输出方向就是数据从程序流向哪里。标准输出方向是指数据从程序流向显示器,如果改变了它的方向,数据就流向其它地方,这就是输出重定向。

2.硬件设备和文件描述符

计算机的硬件设备有很多,常见的输入设备有键盘、鼠标、麦克风、手写板等,输出设备有显示器、投影仪、打印机等。不过,在 Linux 中,标准输入设备指的是键盘,标准输出设备指的是显示器
Linux系统中把一切都看做文件,包括普通文件-、目录文件d、字符设备文件c、块设备文件b、符号链接文件l以及标准输入设备(键盘)和标准输出设备(显示器)在内的所有计算机硬件都是文件

文件描述符是内核为了高效管理已被打开的文件所创建的索引(一个非负整数),用于指代已被打开的文件。

Linux下所有的的I/O操作的系统调用都是通过文件描述符执行,一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。例如0表示标准输入(键盘)、1表示标准输出(显示器)、2表示标准错误(显示器),文件描述符会在这个基础上递增。
stdin、stdout、stderr 默认都是打开的,在重定向的过程中,0、1、2 这三个文件描述符可以直接使用。

表1:与输入输出有关的文件描述符

文件描述符文件名类型硬件
0 stdin 标准输入文件 键盘
1 stdout 标准输出文件 显示器
2 stderr 标准错误输出文件 显示器

 

文件描述符到底是什么

文件描述符: 在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。

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

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

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

 

文件描述符表: 进程级的列表,也就是用户区的一部分,进程每打开一个文件就会新建一个文件描述符。
系统级打开文件表: 系统级的列表,对当前系统的所有进程都共享,每条条目包含文件偏移量、访问模式以及指向它的文件描述符的条目计数
文件系统索引节点表: inode索引节点表(UID、GID、ctime、mtime、atime、读写执行权限、链接数、block位置)

进程级文件描述符系统级打开文件表i-node表

1.文件描述符标志

2.文件指针(open file handle)

通过文件描述符,可以找到文件指针,从而进入打开文件表

1.文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。

2.状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。

3.i-node 表指针。

要想真正读写文件,要通过打开文件表的 i-node 指针进入 i-node 表

1.文件类型,例如常规文件、套接字或 FIFO。

2.文件大小。

3.时间戳,比如创建时间、更新时间。

4.文件锁。

 

对上图的进一步说明:

  • 在进程 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 Shell 输出重定向

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

注意点:

  1. 输出重定向的写法是fd>file或者fd>>file,其中 fd 表示文件描述符,如果不写,默认为 1,也就是标准输出文件。即command 1>file与command >file相同,当文件描述符为大于 1 的值时,比如 2,就必须写上。
  2. fd>之间不能有空格,否则 Shell 会解析失败;>file之间的空格可有可无
  3. /dev/null 文件。如果想要将输出结果丢弃,可以将命令结果重定向到 /dev/null 文件中,即 ls -l &>/dev/null,任何放入垃圾箱的数据都会被丢弃,不能恢复。
 命令结果实际执行
1>之间有空格

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

cat log.txt

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

表2:Bash 支持的输出重定向符号

类 型符 号作 用举栗
标准输出重定向 command >file 以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。

ls -l命令的输出结果重定向到文件中。

[c.biancheng.net]$ ls -l  #先预览一下输出结果
总用量 16
drwxr-xr-x. 2 root     root      21 7月   1 2016 abc
-rw-r--r--. 1 mozhiyan mozhiyan 399 3月  11 17:12 demo.sh
-rw-rw-r--. 1 mozhiyan mozhiyan  67 3月  22 17:16 demo.txt

[c.biancheng.net]$ ls -l >demo.txt  #重定向
[c.biancheng.net]$ cat demo.txt  #查看文件内容
总用量 12
drwxr-xr-x. 2 root     root      21 7月   1 2016 abc
-rw-r--r--. 1 mozhiyan mozhiyan 399 3月  11 17:12 demo.sh
-rw-rw-r--. 1 mozhiyan mozhiyan   0 3月  22 17:21 demo.txt
command >>file 以追加的方式,把 command 的正确输出结果输出到 file 文件中。

将 echo 命令的输出结果以追加的方式写入到 demo.txt 文件中。

  1. #!/bin/bash
  2. for str in "C语言中文网" "http://c.biancheng.net/"成立7年了" 
  3. do
  4.   echo $str >>demo.txt #将输入结果以追加的方式重定向到文件
  5. done

运行脚本,查看文件demo.txt内容,显示如下:
C语言中文网
http://c.biancheng.net/
成立7年了

标准错误输出重定向 command 2>file 以覆盖的方式,把 command 的错误信息输出到 file 文件中。

命令正确执行是没有错误信息的,我们必须刻意地让命令执行出错,如下所示:

[c.biancheng.net]$ ls java  #先预览一下错误信息
ls: 无法访问java: 没有那个文件或目录
[c.biancheng.net]$ ls java 2>err.log  #重定向
[c.biancheng.net]$ cat err.log  #查看文件
ls: 无法访问java: 没有那个文件或目录
command 2>>file 以追加的方式,把 command 的错误信息输出到 file 文件中。  
正确输出和错误信息同时保存 command >file 2>&1 覆盖的方式,把正确输出和错误信息同时保存到同一个文件(file)中。

正确输出和错误信息同时保存

【实例1】把正确结果和错误信息都保存到一个文件中,例如:

[c.biancheng.net]$ ls -l >out.log 2>&1
[c.biancheng.net]$ ls java >>out.log 2>&1
[c.biancheng.net]$ cat out.log
总用量 12
drwxr-xr-x. 2 root     root      21 7月   1 2016 abc
-rw-r--r--. 1 mozhiyan mozhiyan 399 3月  11 17:12 demo.sh
-rw-rw-r--. 1 mozhiyan mozhiyan 278 3月  16 17:17 main.c
ls: 无法访问java: 没有那个文件或目录

out.log 的最后一行是错误信息,其它行都是正确的输出结果。

command >>file 2>&1 追加的方式,把正确输出和错误信息同时保存到同一个文件(file)中。  
command >file1 2>file2 以覆盖的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。  
command >>file1  2>>file2 以追加的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。

上面的实例将正确结果和错误信息都写入同一个文件中,这样会导致视觉上的混乱,不利于以后的检索,

所以我建议把正确结果和错误信息分开保存到不同的文件中,也即写成下面的形式:

ls -l >>out.log 2>>err.log

这样一来,正确的输出结果会写入到 out.log,而错误的信息则会写入到 err.log。

command >file 2>file 不推荐】这两种写法会导致 file 被打开两次,引起资源竞争,所以 stdout 和 stderr 会互相覆盖  
command >>file 2>>file  

Linux Shell 输入重定向

输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而是使用文件作为命令的输入。
和输出重定向类似,输入重定向的完整写法是fd<file,其中 fd 表示文件描述符,如果不写,默认为 0,也就是标准输入文件。

表3:Bash 支持的输出重定向符号

符号说明举栗
command <file 将 file 文件中的内容作为 command 的输入。 统计 readme.txt 文件中有多少行文本:
[c.biancheng.net]$ cat readme.txt  #预览一下文件内容
C语言中文网
http://c.biancheng.net/
成立7年了
[c.biancheng.net]$ wc -l <readme.txt  #输入重定向
3
command <<END

从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止

(分界符可以是任意的字符串,用户自己定义)。

 

输入重定向符号<<,这个符号的作用是使用特定的分界符作为命令输入的结束标志,而不使用 Ctrl+D 键。

<<之后的分界符可以自由定义,只要再碰到相同的分界符,两个分界符之间的内容将作为命令的输入(不包括分界符本身)

统计用户在终端输入的文本的行数。

[c.biancheng.net]$ wc -l <<END

> 123
> 789
> abc
> END
3

wc 命令会一直等待用输入,直到遇见分界符 END 才结束读取。

command <file1 >file2 将 file1 作为 command 的输入,并将 command 的处理结果输出到 file2。  

代码块重定向

{}<file1

  逐行读取文件内容。
#!/bin/bash

while read str; do
    echo $str
done <readme.txt
运行结果:
C语言中文网
http://c.biancheng.net/
成立7年了
日IP数万

总结:

1>log.txt 2>&1 将输出全重定向到log.txt中
  • 本来1----->屏幕 (1指向屏幕)
  • 执行>log后, 1----->log (1指向log)
  • 执行2>&1后, 2----->1 (2指向1,而1指向log,因此2也指向了log)
可以缩写为&>log 或者 >&log
 
2>&1 1>log.txt 将标准错误输出重定向到屏幕,将标准输出重定向到log.txt中
  • 本来1----->屏幕 (1指向屏幕)
  • 执行2>&1后, 2----->1 (2指向1,而1指向屏幕,因此2也指向了屏幕)
  • 执行>log后, 1----->log (1指向log,2还是指向屏幕)
>/dev/null 2>&1  将输出全定向到/dev/null
  • /dev/null 代表空设备文件
  • 2 表示stderr标准错误,& 表示等同于的意思,2>&1,表示2的输出重定向等同于1
  • 1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于 "1>/dev/null"
最常用的方式有:command > file 2>file  与command > file 2>&1
它们有什么不同的地方吗?
首先command >file 2>file 的意思是将命令所产生的stdout和stderr送到file中,file会被打开两次,stdout和stderr会互相覆盖,这样写相当使用了FD1和FD2两个同时去抢占file 的管道
command >file 2>&1 这条命令将stdout直接送向file,stderr 继承了FD1管道后再被送往file,此时,file 只被打开了一次,也只使用了一个管道FD1,它包括了stdout和stderr的内容。
从IO效率上,前一条命令的效率要比后面一条的命令效率要低,所以在编写shell脚本的时候,较多的时候我们会command > file 2>&1 这样的写法。

参考文献: https://www.cnblogs.com/yujianfei/p/9068472.html

 

推荐阅读