本文目录:
1.1 文件描述符(file description,fd)
1.2 文件描述符的复制
1.3 重定向顺序很重要:">file 2>&1" 和 "2>&1>file"
1.4 改变当前 shell 环境的重定向目标
1.5 关闭文件描述符
1.6 打开文件
1.7 文件描述符的移动
1.8 经典示例
基本的重定向功能想必都理解. 本文对 shell 环境下的 IO 重定向稍作深入, 相信看完后, 能够彻底理解 >file 2>&1 .
1.1 文件描述符(file description,fd)
文件描述符是 IO 重定向中的重要概念. 文件描述符使用数字表示, 它指明了数据的流向特征.
软件设计认为, 程序应该有一个数据来源, 数据出口和报告错误的地方. 在 Linux 系统中, 它们分别使用描述符 0,1,2 来表示, 这 3 个描述符默认的目标文件 (设备) 分别是 / 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/
total 0
- lrwx------ 1 root root 64 Apr 6 03:53 0 -> /dev/pts/2
- lrwx------ 1 root root 64 Apr 6 03:53 1 -> /dev/pts/2
- lrwx------ 1 root root 64 Apr 6 03:53 2 -> /dev/pts/2
- lr-x------ 1 root root 64 Apr 6 03:53 3 -> /proc/14038/fd
在 Linux 中, 每一个进程打开时都会自动获取 3 个文件描述符 0,1 和 2, 分别表示标准输入, 标准输出, 和标准错误, 如果要打开其他文件, 则文件描述符必须从 3 开始标识. 对于我们人为要打开的描述符, 建议使用 9 以内的描述符, 超过 9 的描述符可能已经被系统内部分配给其他进程.
文件描述符说白了就是系统为了跟踪这个打开的文件而分配给它的一个数字, 这个数字和文件绑定在一起, 数据流入描述符的时候也表示流入文件.
而 Linux 中万物皆文件, 这些文件都可以分配描述符, 包括套接字.
程序在打开文件描述符的时候, 有三种可能的行为: 从描述符中读, 向描述符中写, 可读也可写. 从 lsof 的 FD 列可以看出程序打开这个文件是为了从中读数据, 还是向其中写数据, 亦或是既读又写. 例如, tail 命令监控文件时, 就是打开文件从中读数据的(3r 的 r 是 read,w 是 write,u 是 read and write).
[root@mariadb ~]# lsof -n | grep "/a.sh" | column -t
tail 13563 root 3r REG 8,2 182 69632966 /root/a.sh
1.2 文件描述符的复制(duplicate)
文件描述符的复制表示复制文件描述符到另一个文件描述符中以作副本文件. 使用 "&" 进行复制. 如果不好理解复制的意思, 将其理解为 "绑定","重用".
[n]<&word : 将文件描述符 n 绑定到 word 代表的文件或描述符. 可以理解为文件描述符 n 重用 word 代表的文件或描述符. n 不指定则默认为 0(标准输入就是 0), 表示标准输入也将输入到 word 所代表的文件或描述符中.
[n]>&word : 将文件描述符 n 绑定到 word 代表的文件或描述符. 可以理解为文件描述符 n 重用 word 代表的文件或描述符. n 不指定则默认为 1(标准输出就是 1), 表示标准输出也将输出到 word 所代表的文件或描述符中.
注意, 每绑定一次, 都会立刻重定向到对应的文件. 请结合下面的例子感受.
例如, 3>&1 表示 fd=3 绑定到 fd=1 上, 而 fd=1 目前的重定向目标文件是 / dev/stdout, 因此 fd=3 也重定向到 / dev/stdout, 以后进程将数据写入 fd=3 的时候, 将直接输出到屏幕. 如果用 "复制" 来理解, 就是 fd=3 是当前 fd=1 的一个副本, 即指向 / dev/stdout 设备. 如果后面改变了 fd=1 的输出目标(如 file1), 由于 fd=3 的目标仍然是 / dev/stdout, 所以可以拿 fd=3 来还原 fd=1 使其目标变回 / dev/stdout.
再例如, cat <&1 表示 fd=0 绑定到 fd=1 上, 而此时 fd=1 的重定向文件是 / dev/stdout, 因此此时 / dev/stdout 既是标准输入设备, 也是标准输出设备, 也就是说进程从 / dev/stdout(屏幕)接受输入, 输入后再直接输出到 / dev/stdout. 以下是结果:
[root@mariadb ~]# cat <&1
q # 进入交互式, 输入数据
q # 直接输出
1.3 重定向顺序很重要:">file 2>&1" 和 "2>&1>file"
想必很多人都知道 >file 2>&1 的作用, 它等价于 &>file , 表示标准输出和标准错误都重定向到 file 中. 那它和 2>&1>file 有什么区别呢?
首先解释 >file 2>&1 . 这里分两个过程: 先打开 file, 再将 fd=1 重定向到 file 文件上, 这样 file 文件就成了标准输出的输出目标; 之后再将 fd=2 绑定 (注意, 是绑定不是重定向) 到 fd=1 上, 而 fd=1 此时已经重定向到 file 文件上, 因此 fd=2 也重定向到 file 上. 所以, 最终的结果是标准输出重定向到 file 上, 标准错误也重定向到 file 上.
再解释 2>&1>file . 这里也分两个过程: 先将 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: cannot access sjdfk: No such file or directory
最后, 也许你已经发现了, 绑定和重定向是不同的, 绑定不应该称为重定向. 区分这两个概念, 在实际应用的过程中能解决非常多的疑惑. 在本文结尾的最后一个例子中, 你将能非常明确地体会到绑定和重定向的区别.
1.4 改变当前 shell 环境的重定向目标
如果在命令中直接改变重定向的位置, 那么命令执行结束的时候描述符会自动还原. 正如上面的 ls /boot 2>&1>/tmp/a.log 命令, 在 ls 执行结束后, fd=2 还原回默认的 / dev/stderr,fd=1 还原回默认的 / dev/stdout.
但是我们可以通过 exec 程序直接在当前的 shell 环境下改变重定向目标, 只有在当前 shell 退出的时候才会释放描述符的绑定.
例如: 下面的命令将标准错误 fd=2 重定向到 fd=3 对应的文件上.
exec 2>&3
因此, 我们可能在一段程序执行结束后, 需要将描述符还原到原来的位置, 并关闭不再需要的描述符. 毕竟描述符也是资源, 是有限的(ulimit -n).
1.5 关闭文件描述符
[n]>&- [n]<&-
关闭文件描述符的方式是将 [n]>&word 和 [n]<&word 中的 word 使用符号 "-", 这表示释放 fd=n 描述符, 且关闭其指向的文件.
1.6 打开文件
[n]<> filename : 打开 filename, 并指定其文件描述符为 n, 该描述符是可读, 可写的描述符. 若不指定 n 则默认为 0, 若 filename 文件不存在, 则先创建 filename 文件.
例如:
[root@mariadb ~]# exec 3<> /tmp/a.log
[root@mariadb ~]# lsof -n | grep "/a.log" | column -t
bash 13637 root 3u REG 8,2 292018 69632965 /tmp/a.log
如果再 exec 1>&3 将 fd=0 绑定到 fd=3 上, 那么 / tmp/a.log 就成了标准输入的来源.
1.7 文件描述符的移动
文件描述符的移动表示将文件描述符 1 移动到描述符 2 上, 同时关闭文件描述符 1.
[n]>&digit- : 将文件描述符 digit 代表的输出文件移动到 n 上, 并关闭 digit 值的描述符.
[n]<&digit- : 将文件描述符 digit 代表的输入文件移动到 n 上, 并关闭 digit 值的描述符.
例如:
[root@mariadb ~]# exec 3<> /tmp/a.log
[root@mariadb ~]# lsof -n | grep "/a.log" | column -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" | column -t # 在另一个 bash 窗口查看
bash 13637 root 1u REG 8,2 292018 69632965 /tmp/a.log
可见, fd=3 移动到 fd=1 后, 原本与 fd=3 关联的 / tmp/a.log 已经关联到 fd=1 上.
1.8 经典示例
(1). 示例一:
以下是Advanced Bash-Scripting Guide中的示例:
- echo 1234567890> File # (1). 写字符串到 "File".
- exec 3<> File # (2). 打开 "File" 并且给它分配 fd 3.
read -n 4 <&3 # (3). 只读 4 个字符.
- echo -n .>&3 # (4). 写一个小数点.
- exec 3>&- # (5). 关闭 fd 3.
cat File # (6).1234.67890
(1)向文件 File 中写入几个字符.
(2)打开文件 File 以备 read/write, 并分配 fd=3 给该文件.
(3)将 fd=0 绑定到 fd=3 上, 而 fd=3 的重定向目标为 File, 所以 fd=0 的目标也是 File, 即从 File 中读取数据. 这里读取 4 个字符, 由于 read 命令中没有指定变量, 因此分配给默认变量 REPLY. 注意, 这个命令执行结束后, fd=0 的重定向目标会变回 / dev/stdin.
(4)将 fd=1 绑定到 fd=3 上, 而 fd=3 的重定向目标文件为 File, 所以 fd=1 的目标也是 File, 即数据写入到 File 中. 这里写入一个小数点. 注意, 这个命令结束后, fd=1 的重定向目标回变回 / dev/stdout.
(5)关闭 fd=3, 这也会关闭其指向的文件 File.
(6)File 文件中已经写入了一个小数点. 如果此时执行 echo $REPLY , 将输出 "1234".
(2). 示例二: 关于描述符恢复, 关闭
exec 6>&1 # (1)
exec> /tmp/file.txt # (2)
- echo "---------------" # (3)
- exec 1>&6 6>&- # (4)
- echo "===============" # (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
echo "---------------"
exec>/dev/pts/2
echo "==============="
如果不借用 fd=6 这个中转描述符, 你要先去获取并记住当前 shell 所在的终端, 很不方便. 而且, 如果要恢复的不是 fd={0,1,2}, 那就更麻烦.
最后给张描述符复制, 恢复的过程实例图:
回到 Linux 系列文章大纲: http://www.cnblogs.com/f-ck-need-u/p/7048359.html
回到网站架构系列文章大纲: http://www.cnblogs.com/f-ck-need-u/p/7576137.html
回到数据库系列文章大纲: http://www.cnblogs.com/f-ck-need-u/p/7586194.html
来源: https://www.cnblogs.com/f-ck-need-u/p/8727401.html