Linux 中每个进程维护了一个文件描述符表,如 shell 终端,默认有三个文件已打开,他们的文件描述符和文件对应关系为:
1 | fd 0: /dev/stdin -> /proc/self/fd/0(前者是指向后者的软链接,下同) |
所以在该 shell 中创建新的的文件的描述符就从3开始。
用于输入输出重定向使用的文件描述符大于9,所以安全可用的自定义文件描述符范围就是:3-9
因为 Bash 文档 里有这样一句话:
Redirections using file descriptors greater than 9 should be used with care, as they may conflict with file descriptors the shell uses internally.
下面的几个例子会分别演示
1)输入输出重定向
2)复制输入输出文件描述符
3)移动输入输出文件描述符
4)关闭文件描述符和同时读写文件
5)将已重定向的输入/输出恢复到标准输入/标准输出
脚本例子1
这个例子讲的是 输出重定向
和 复制输出文件描述符
脚本内容:
1 |
|
输出:
1 | [root@jz ~]# sh test.sh 3>file3 4>file4 5>file5 6>file6 |
说明:
创建自定义的文件描述符(fd)从 3 开始。test.sh 脚本中 >&n(n 表示文件描述符,为 3, 4, 5, 6)表示:
复制 fd n 作为 fd 1(1 即标准输出的文件描述符)的值(这里的复制可以理解为复制 fd n 指向的文件指针,fd n 的值 n 实际上是数组下标,数组元素才是指向文件的指针)
也就是说,fd 1 复制了 fd n 的值之后,fd 1 原本指向文件 /dev/stdout,而现在与 fd n 指向相同的文件,n 必须与实际文件关联,这个文件描述符 n 才是有效的
在上文的例子中,通过执行脚本 test.sh,fd 3 关联(重定向)到了实际文件 file3,fd 4 关联到了实际文件 file4 等等
fd 3, 4, 5, 6 关联了实际文件之后,脚本中的 echo “fdN” >&N 就可以依次把标准输出的内容保存到不同的文件了
脚本例子2
这个例子讲的是 输入重定向
和 复制输入文件描述符
脚本内容:
1 |
|
输出:
1 | [root@jz ~]# sh test.sh 3<file3 4<file4 |
说明:
和输出重定向差不多,例子2脚本中标准输入分别复制了:输入文件描述符3、输入文件描述符4,使得标准输入分别指向 file3、file4
执行脚本时,fd 3、fd 4 分别重定向到了 file3、file4,所以脚本中 cat <&N 就会依次从 file3、file4 读取内容(file3、file 4 是例子1脚本执行后创建的)
上面两个例子讲的是“复制文件描述符”和“输入输出重定向”,bash shell 也支持“移动文件描述符”,下面的脚本将实验移动文件描述符。
脚本例子1
这个例子讲的是 移动输出文件描述符
脚本:
1 |
|
执行:
1 | [root@jz ~]# cat file3 |
说明:
1)与复制文件描述符不同的是,脚本中 >&3- 表示在 fd 1 复制了 fd 3 的文件指针以后,fd 3 就会关闭,就好像 fd 3 移动到了 fd 1 一样
注意脚本执行结果,第1次写入内容到 fd 3 是正常的,第2次写入时也正常,但之后 fd 3 就被关闭,即 fd 3 不再重定向到文件 file3
所以第3次写入内容时,就会报错,显示坏掉的文件描述符
2)还有,一开始 file3 只有一行,最后变成了二行,因为脚本中的 echo 操作的文件从始至终都是 file3,脚本执行时,因为输出文件 fd 3 重定向到 file3
脚本开始执行,file3 被打开,直到脚本结束,file3 文件才被关闭,所以脚本中每次执行完 echo “fd3” >&3[-] 都是向 file3 新增一行
而不会像终端下直接执行:echo “fd3” > file3,每次都是覆盖文件,因为终端下每次执行 echo 都会单独打开/关闭文件 file3
脚本例子2
这个例子讲的是 移动输入文件描述符
脚本:
1 |
|
执行:
1 | [root@jz ~]# cat file3 |
说明:
脚本中使用 read 命令而不是 cat,因为 read 可以逐行读取文件,而 cat 会一次把文件所有行都读取完
类似脚本例子1,这里 <&3- 把输入文件描述符3关闭之后,下次执行 read 命令就会报错
如果脚本例子2内容如下:
1 |
|
输出结果是:
1 | [root@jz ~]# sh move_fd.sh 3<file3 |
可以看到,cat 并不会报错,可能是在 cat 一次读完文件后,cat 先判断文件是否读完,再判断文件描述符3是否可用(因为已读完就无需判断是否可用),所以不会报错
创建文件描述符来同时读写文件
语法:n<>filename
n 为文件描述符
filename 为实际文件名
n 不写时表示 fd 0
举例脚本:
1 |
|
结果:
1 | [root@jz ~]# cat inputfile |
说明:
以上输出结果很复杂,涉及到文件读写操作的实现细节,猜测如下:
脚本执行后,执行到 exec 命令行,文件 inputfile 被打开,文件读写指针应该是指向文件开头的
此时运行 echo 命令行,把 1234 写入到文件开头位置,覆盖掉 abcde 前4个字符变成 1234e,读写指针在字符 e 的位置
执行 ls 时因为文件不存在,信息输出到标准错误,所以文件 inputfile 读写指针位置不变
执行 cat 从字符 e 开始读取,直到文件结尾,读到的内容是:
e
dsfsf
因为文件被 exec 打开,cat 并不会关闭文件,cat 读取出来的内容会再次写入 inputfile 文件,此时读写指针在文件结尾
cat 读出的内容添加到文件结尾之后,变成:
1234e
dsfsf
e
dsfsf
关闭文件描述符
1 | <&- 关闭标准输入 |
举例脚本:
1 | [root@jz ~]# cat <&- # 关闭标准输入 |
最后,说说怎么恢复到标准输入/标准输出
其实很简单,上文已经讲了复制文件描述符,只需要把标准输入输出,或标准错误先复制保存,之后恢复即可。
举例脚本:
1 |
|
执行:
1 | [root@jz ~]# cat file3 |
那么,怎么把 read 的输入恢复到标准输入,再次询问用户按回车继续呢?
把脚本改成:
1 |
|
然后用如下方式执行脚本:
1 | sh test.sh 3<&0 <file3 |
执行脚本时,它的含义是先创建一个新的文件描述符3,fd 3 复制 fd 0 的值,就把 fd 0 保存到 fd 3 了,然后在脚本中 read 命令的标准输入文件描述符从 fd 3 复制回来即可。
参考:
https://www.gnu.org/software/bash/manual/html_node/Redirections.html