bash&shell系列文章:http://www.hack95.com/f-ck-need-u/p/7048359.html


你必须了解基本的重定向功能。本文更深入地介绍了 shell 环境中的 IO 重定向。相信看完之后你一定能够完全理解>文件2>&1

文件描述符(文件描述,fd)

文件描述符是IO重定向中的一个重要概念。文件描述符由数字表示,它指定了数据的流向特征。

软件设计认为,一个程序应该有数据源、数据导出、报告错误的地方。在Linux系统中,它们分别用描述符0、1、2来表示。这三个描述符的默认目标文件(设备)是/dev/stdin、/dev/stdout、/dev/stderr。它们分别是到各个终端字符设备的软链接。

[root@mariadb ~]# ll /dev/std*
lrwxrwxrwx 1 root root 15 Apr 2 07:57 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Apr 2 07:57 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Apr 2 07:57 /dev/stdout -> /proc/self/fd/1

[root@mariadb ~]# ll /proc/self/fd/
总计0
lrwx------ 164 四月 6 03:53 0 -> /dev/pts/ 2
lrwx------ 164 四月 6 03:53 1 -> /dev/pts/ 2lrwx------ 164 四月 6 03:53 2 -> /dev/pts/2
lr-x------ 1 root root 64 四月 6 03:53 3 -> /proc/14038/fd

在Linux中,每个进程打开时,都会自动获取三个文件描述符0、1、2,分别代表标准输入、标准输出、标准错误。如果要打开其他文件,文件描述符必须从 3 开始标记。对于我们要人为打开的描述符,建议使用9个以内的描述符。超过9个的描述符可能已经被系统内部分配给其他进程了。

说白了,文件描述符就是系统为了跟踪打开的文件而分配给它的一个数字。该号码与文件绑定。当数据流入描述符时,也意味着流入文件。

Linux 中的一切都是文件,这些文件可以被分配描述符,包括套接字。

当程序打开一个文件描述符时,它有三种可能的行为:从描述符中读取、向描述符中写入以及同时读取和写入。从lsof的FD列中,我们可以看到程序打开这个文件是从中读取数据,向其中写入数据,还是同时读取和写入。例如,当tail命令监视一个文件时,它会打开该文件并从中读取数据(3r中的r代表读,w代表写,u代表读和写)。

[root@mariadb ~]# lsof -n | grep "/www.hack95.com"|列-t
尾部 13563 根 3r REG 8,2 182 69632966 /root/www.hack95.com

文件描述符重复

复制文件描述符是指将一个文件描述符复制到另一个文件描述符中进行复制。使用“&”进行复制。

[n]<&word:将文件描述符n复制到word表示的文件或描述符中。可以理解为,文件描述符n复用了word所代表的文件或描述符,即word原本对应的是哪个文件,现在n作为其副本也对应了这个文件。如果不指定n,则默认为0(标准输入为0),这意味着标准输入也会输入到word表示的文件或描述符中。 

[n]>&word: 将文件描述符n复制到word表示的文件或描述符中。可以理解为,文件描述符n复用了word所代表的文件或描述符,即word原本对应的是哪个文件,现在n作为其副本也对应了这个文件。如果不指定n,则默认为1(标准输出为1),这意味着标准输出也会输出到word表示的文件或描述符中。 

例如3>&1表示将fd=3复制到fd=1,fd=1的当前重定向目标文件为/dev/stdout(fd=1指向默认)输出设备),因此 fd=3 也被重定向到 /dev/stdout。以后进程向fd=3写入数据时,会直接输出到屏幕上。这里 3>&1 相当于 3>&/dev/stdout。如果我们用“复制”来理解,fd=3就是当前fd=1的副本,它指向/dev/stdout设备。如果后面fd=1的输出目标发生了变化(比如file1),由于fd=3的目标仍然是/dev/stdout,所以可以使用fd=3恢复fd=1,将目标改回/dev /标准输出。

(fd=1) --> /dev/stdout
  |
 3>&1
 \|/
(fd=3) --> /dev/stdout

关于文件描述符的重复

在操作系统(或C)中,对于实体文件的文件描述符来说,文件描述符用于描述它所指向的实体文件。例如,fd=5 指向文件a.txt。 Duplicate实际上执行的是dup()函数,意思是创建另一个文件描述符(例如fd=6),指向同一个底层对象,比如指向同一个实体文件。此时fd=5和fd=6都会指向a.txt。

在shell中,我们将文件描述符和实体文件之间的关系(或指向关系)称为重定向。其实用更底层的指向关系更容易理解。例如“3>&1”表示复制fd=1,使得fd=3和fd=1都指向同一个对象,即stdout。

再举个例子,cat <&1表示将fd=0复制到fd=1。此时fd=1的重定向文件是/dev/stdout,所以fd=0也指向这个/dev/stdout文件,而cat是从fd=0读取标准输入,所以/dev/stdout既是一个标准输入设备和标准输出设备,也就是说进程从/dev/stdout(屏幕)接受输入,然后直接输出到/dev/stdout。结果如下:

[root@mariadb ~]#猫<&1
q # 进入交互并输入数据
q # 直接输出

最后需要说明的是一种特殊情况,如果是>&word,并且word不是数值,比如echo haha​​​​>&/tmp/ a.log ,那么 >&word&>word 是等价的,都代表 >字2>&1。请参阅 man bash 的“重定向标准输出和标准错误”段落。

重定向顺序很重要:“>文件 2>&1”和“2>&1 >文件”

相信很多人都知道>文件2>&1的作用。相当于&>file,表示标准输出和标准错误都被重定向到文件。那么它和2>&1>file有什么区别呢?

先解释一下>文件2>&1。这里有两个过程:首先打开文件,然后将fd=1重定向到文件file,这样文件file就成为标准输出的输出目标;然后将fd=2复制到fd=1,而fd=1此时已经重定向到文件file,所以fd=2也重定向到文件。因此,最终的结果是标准输出被重定向到文件,标准错误也被重定向到文件。

解释2>&1>文件。这里也有两个过程:首先将fd=2复制到fd=1,而fd=1重定向的文件是默认的/dev/stdout,所以fd=2也重定向到/dev/stdout;然后然后将fd=1重定向到文件file。也就是说,这里的标准错误和标准输出仍然是分开输出的,只不过用/dev/stdout代替了/dev/stderr,用file代替了/dev/stdout。所以,最终的结果是标准错误输出到/dev/stdout,即屏幕上,标准输出会输出到文件file。

可以使用以下命令来测试2>&1 >file。第一个ls命令是正确的,结果将输出到/tmp/a.log。第二个ls命令不正确,结果会直接输出到屏幕上。

[root@mariadb ~]# ls /boot 2>&1 >/tmp/a.log
[root@mariadb ~]# ls sjdfk 2>&1 >/tmp/a.log
ls: 无法访问 sjdfk: 没有这样的文件或目录

更改当前shell环境的重定向目标

如果直接在命令中更改重定向位置,则命令执行结束时描述符会自动恢复。就像上面的ls /boot 2>&1 >/tmp/a.log命令一样,执行ls后,fd=2恢复到默认的/dev/stderr,fd=1恢复回默认的/dev/stderr到默认的/dev/stdout。

但是我们可以通过exec程序直接在当前shell环境中更改重定向目标。仅当当前 shell 退出时,描述符绑定才会被释放。

例如:以下命令将标准错误 fd=2 指向 fd=3 对应的文件。

执行2>&3

因此,我们可能需要在程序执行后将描述符恢复到原来的位置,并关闭不再需要的描述符。毕竟,描述符也是资源并且是有限的(ulimit -n)。

关闭文件描述符

[n]>&-
[n]<&-

关闭文件描述符的方法是对[n]>&word[n]<&word中的单词使用符号“-”,表示释放fd=n 描述字符并关闭它指向的文件。

打开文件

[n]<> 文件名: 打开文件名并将其文件描述符指定为 n,这是一个可读可写的描述符。如果不指定n,则默认为0。如果filename文件不存在,则首先创建filename文件。

例如:

[root@mariadb ~]# exec 3<> /tmp/a.log
[root@mariadb ~]# lsof -n | grep "/a.log"|列-t
bash 13637 root 3u REG 8,2 292018 69632965 /tmp/a.log 

如果exec 1>&3将fd=1复制到fd=3,则/tmp/a.log成为标准输出的目标。

文件描述符的移动

文件描述符的移动意味着将文件描述符1移动到描述符2,同时关闭文件描述符1。

[n]>&digit-: 将文件描述符数字表示的输出文件移动到 n 并关闭数字值的描述符。

[n]<&digit-: 将文件描述符数字表示的输入文件移动到 n 并关闭数字值的描述符。

例如:

[root@mariadb ~]# exec 3<> /tmp/a.log
[root@mariadb ~]# lsof -n | grep "/a.log"|列-t
bash 13637 root 3u REG 8,2 292018 69632965 /tmp/a.log
[root@mariadb
~]# exec 1>&3- # 将 3 移至 1,关闭 3 [root@mariadb ~]# lsof -n | grep "/a.log"|另一个 bash 窗口视图中的列 -t # bash 13637 root 1u REG 8,2 292018 69632965 /tmp/a.log

可以看到,fd=3移动到fd=1后,原来与fd=3关联的/tmp/a.log已经与fd=1关联起来了。

经典范例

(1)。例1:

以下是《Advanced Bash-Scripting Guide》的示例:

回显1234567890 > 文件编号 (1)。将字符串写入 "File"。
exec 3<> 文件 # (2)。打开 "文件" 并将其分配给 fd 3。
阅读 -n 4 <&3 # (3)。只读 4 个字符。
回声-n。 >&3 # (4)。写一个小数点。
exec 3>&- # (5)。关闭 fd 3。
cat 文件# (6).1234.67890

(1)向文件File中写入几个字符。

(2) 打开文件 File 进行读/写,并赋予文件 fd=3。

(3) 将fd=0复制到fd=3,fd=3的重定向目标是File,所以fd=0的目标也是File,即从File中读取数据。这里读取4个字符。由于读取命令中没有指定变量,因此将其分配给默认变量 REPLY。注意,执行该命令后,fd=0的重定向目标将变回/dev/stdin。

(4) 将fd=1复制到fd=3,fd=3的重定向目标文件为File,所以fd=1的目标也是File,即数据为写入文件中。在这里写一个小数点。注意,该命令结束后,fd=1的重定向目标将变回/dev/stdout。

(5) 关闭fd=3,这也会关闭它指向的文件。

(6) File 文件中已写入小数点。如果此时执行echo $REPLY,则会输出“1234”。

(2)。示例2:关于描述符恢复和关闭

执行6>&1# (1)
执行 > /tmp/file.txt # (2)回声---------------#(3)
执行 1>&6 6>&- # (4)
回声===============#(5

(1) 首先将fd=6复制到fd=1。此时fd=1的重定向目标是/dev/stdout,所以fd=6的重定向目标是/dev/stdout。
(2) 将 fd=1 重定向到 /tmp/file.txt 文件。此后的所有标准输出都将写入/tmp/file.txt。
(3)写入数据。数据将写入/tmp/file.txt。
(4) 将 fd=1 复制回 fd=6。此时fd=6的重定向目标是/dev/stdout,所以fd=1会恢复到/dev/stdout。最后关闭fd=6。
(5)写入数据,该数据将输出到屏幕上。

也许你想知道为什么需要先将fd=1复制到fd=6,然后使用fd=6恢复fd=1。恢复时,可以将 fd=1 重定向回 /dev/stdout 吗?然而?

其实这里借用了传输描述符fd=6,是为了操作方便。你不必使用它,但是当恢复fd=1的重定向目标时,你应该重定向到`/dev/{伪终端字符设备}`而不是/dev/stdout,因为/dev/stdout是一个软终端关联。它的目标指向/proc/self/fd/1,但该文件仍然是软链接,指向/dev/{伪终端字符设备}。同样,/dev/stdin 和 /dev/stderr 是相同的。

因此,如果您当前所在的终端是pts/2,则可以使用以下命令来实现与上面相同的功能:

exec > /tmp/file.txt
回声---------------
执行>/dev/pts/2echo "==============="

exec >/dev/tty # 这样比较方便

如果不借用传输描述符fd=6,则必须先获取并记住当前shell所在的终端,非常不方便。但你可以用文件/dev/tty来代表当前终端,这样就方便多了。

但是如果你要恢复的文件不是终端相关的文件,你可能只能通过备份和恢复文件描述符来恢复它们。

最后举一个描述符复制和恢复过程的例子:

使用变量作为文件描述符

有时候在一些特殊的需求下,你可能会想用变量来保存分配的文件描述符,这样多个手动打开的文件夹描述符就不会混淆。

要使用变量保存文件描述符,可以使用以下方法:

fd=3
评估exec ${fd}<> /tmp/a.log
lsof -n | grep a.log

bash 4.1之后,bash本身提供了可变文件描述符的功能。只要在需要分配文件描述符时指定原fd为fdvar,就可以创建这个变量并分配文件。描述符会自动保存到变量 fdvar 中。使用此模式时,文件描述符从 10 开始分配,因此 fdvar 是大于或等于 10 的值。

exec {fd1}<> /tmp/a.log
echo $fd1 # 输出:10

执行 {fd2}<> /tmp/a.log
echo $fd2 # 输出:11