1、简单示例
echo 1234567890 > File exec 3<> File read -n 4 <&3 echo -n . >&3 exec 3>&- cat File
在shell中最多可以有9个打开的文件描述符。0 1 2是每个程序都会默认打开的3个fd。其他6个从3~8的文件描述符均可用作输入或输出重定向。可以自由定义。
执行如上shell 会得到什么结果呢?
分析一下
1、将字符串覆盖重定向输出到File中
2、<> 代表可读可写模式,以可读可写的方式打开fd3
3、-n 代表以字节方式读取,从fd=3读取四个字节
-n nchars
read returns after reading nchars characters rather than waiting for a complete line of input, but honors a delimiter if fewer than nchars
-n nchars
read returns after reading nchars characters rather than waiting for a complete
4、不换行将点覆盖冲定向到fd3
5、&- 代表关闭描述符 ,关闭fd3
6、查看File
其中重点在于第三四行:
read和echo共享同一个偏移量指针。read读取四个字节后指针放在数字串5的位置. 接着echo将. 写在了5那个位置。
所以结果是1234.67890
此外read读取的变量一般存在$REPLY变量中。
2、文件描述符的备份与还原
先理解这两个概念:
[n]<&word:将文件描述符n复制于word 代表的文件或描述符。可以理解为文件描述符n重用word代表的文件或描述符,即word原来对应哪个文件,现在n作为它的副本也对应这个文件。n不指定则默认为0(标准输入就是0),表示标准输入也将输入到word所代表的文件或描述符中。 [n]>&word:将文件描述符n复制于word 代表的文件或描述符。可以理解为文件描述符n重用word代表的文件或描述符,即word原来对应哪个文件,现在n作为它的副本也对应这个文件。n不指定则默认为1(标准输出就是1),表示标准输出也将输出到word所代表的文件或描述符中。
exec 6>&1 exec > /tmp/file.txt echo "---------------" exec 1>&6 6>&- echo "==============="
1、exec打开fd6,fd6 指向fd1,fd1就是当前的终端。所以这时候fd1和fd6都指向了当前终端。
2、exec后面省略1. 此时fd1 执行/tmp/file.txt。 fd6还是指向当前终端
3、在当前终端输出字符串 ???错。这时候当前终端不会显示,因为fd1指向的是file.txt,所以终端不会显示任何字符串。
4、将fd1 和fd6 合并指向fd6。那就代表之前fd1 指向tmp/file.txt 的指向断开了。紧接着将fd6关闭,fd1又指向了当前终端。
5、当前终端(fd1)输出字符串
其中第一步属于fd的备份。第四步属于fd的还原。
再看个例子加深下印象
直接将fd1 还原到终端屏幕上来
exec打开fd1 并指向file.txt
如何将fd1还原到当前终端呢?
/proc/self/fd/{0,1,2} -> /dev/pts/N(N是终端号)
不管是标准输入、输出、错误都会指向一个终端,想要fd1重回终端只需要exec 指向当前终端。
输入w在file.txt查看。
exec >/dev/pts/2就可以回到当前终端了。
需要注意:exec开启的fd,类似于一个指针,往其中写一些数据,指针向前移动。即便是覆盖式重定向,也不会出现覆盖的问题。
3、通过高级重定向实现真正的临时文件
平时我们所谓的临时文件,大多数都是先创建然后删除文件,其实这并不是真正的临时文件,只是自己定义的一个概念,那真正的临时文件是什么呢?
创建之后立即删除,维持其fd打开,基于其fd做事情才是真正的临时文件。
#!/bin/bash # open fd=3 and remove file exec 3<> /tmp/${0}${$}.temp rm -rf /tmp/${0}${$}.temp # file deleted ls /proc/self/fd lsof -n | grep -E 'temp.*delete[d]' # write to fd=3 echo "hello world" >&3 # read from fd #cat <&3 cat /proc/self/fd/3 # close fd exec 3<&- lsof -n | grep -E 'temp.*delete[d]'
脚本执行的结果
脚本以及结果的解释:
exec 3<> /tmp/${0}${$}.temp
exec 开启fd3,以可读可写模式执行指定的文件。
- ${0} 代表的当前shell的文件名
- ${$} 代表当前的PID
紧接着删除文件,尽管该文件已经被删除,但是可以通过fd3继续写入,此时写入的数据是存储在内存中的。
ls /proc/self/fd 查看当前进程打开的fd列表。
这个fd是0 1 2 3 4. 为何会是四个呢?
ls 进程是fork自该脚本文件,改脚本文件已经定义了fd3,ls 进程继承了fd3 ,自己打开的fd4。所以共4个fd。
lsof -n查看temp文件确实已经被删除了。此时这种状态代表该fd还在内存中存在,这也是经常用于文件恢复的一个手法。
echo "hello world" >&3 向fd3 写入内容,此时的内容是保存在内存中而不是硬盘中。
如何读取上面写入的内容呢?
cat < &3 是读取不到内容的。该方式读取是从指针指向hello word之后的位置读取,因此读取不到内容。
使用cat /proc/self/fd/3 直接从fd3中读取内容。这代表重新打开fd,指针是从文件的头部开始的。
exec 3<&- 关闭fd3
lsof -n此时已经查看不到改文件了,该文件彻底从内存中消失了。
4、多进程控制
xargs 多进程示例
redirect]# time bash -c 'echo -e "1\n2\n3\n4" | xargs -i -n 1 -P 4 sleep {}' real 0m4.006s user 0m0.002s sys 0m0.004s
分别将1 2 3 4传递给sleep最多4个进程来处理,所以共花费了4s。
bash -c代表执行后面的shell
echo -e 代表启用转义字符。
xargs -i 是用于传递到多个位置时使用
xargs -n 1代表一个分一段
-P 代表最多4个进程跑
去掉多进程参数,则所需时间为1 + 2 + 3 + 4 = 10s
redirect]# time bash -c 'echo -e "1\n2\n3\n4" | xargs -i -n 1 sleep {}' real 0m10.007s user 0m0.005s sys 0m0.002s
看一个多进程的例子:
#!/bin/bash # 脚本中有后台,捕获它们 trap 'kill 0;exit 1' SIGINT SIGTERM # 多少个进程数,默认5 proc_count=5 # 创建临时的命名管道,并打开它 tempfifo=/tmp/temp_${0}_${$}.fifo mkfifo $tempfifo exec 5<> $tempfifo rm -rf $tempfifo # 向命名管道中写入指定进程数量的空行,以空行数量描述进程池,一个空行代表一个进程 for i in $(seq 1 $proc_count);do echo done >&5 # 多进程工作点 while true;do # 从命名管道中读一个空行 read -u5 date +"%T" { sleep 3s; echo >&5; } & #每次执行循环体内的内容进程少一个,需要使用echo >&5向进程池注入一个进程 done; wait #父进程中等待子进程执行完
解释:
mkfifo $tempfifo ;创建于一个命名管道
exec 5<> $tempfifo ; exec打 可读可写打开并分配fd5。 此处是如何知道fd5 是空闲的呢? 可能就是先分配,如果失败就报错了。
运行结果
]# sh -x test2.sh
pgrep -f 'sleep' 查看一直是有五个进程去执行。
也可以使用pstree -p | grep 'sleep' 查看
借鉴这个思路可以实现,多进程ping.
# 从命名管道中读一个空行 read -u5 date +"%T" { ping -c1 -w1 192.168.56.10-254; echo >&5; } &
5、exec创建输出/输入文件描述符
输出重定向
#!/bin/bash # storing STDOUT, then coming back to it exec 3>&1 exec 1>test14out echo "This should store in the output file" echo "along with this line." echo "hehehehehehhe." >&1 echo "hhaaha" >&3 exec 1>&3 echo "Now things should be back to normal" echo "hhe" >&1 echo "aaahhe" >&3
echo "aaahhe" >&3
输入重定向
#!/bin/bash exec 6<&0 # 开启fd 6,并指向STDIN。此时从fd6 中读取数据相当于从STDIN中读取 exec 0< testfile # 让STDIN从testfile读取内容 count=1 while read line;do echo "Line #$count: $line" count=$[ $count + 1 ] done exec 0>&6 #第一步的反操作,意思就是向STDIN写入就是向 fd6写入。如果不把STDIN 换回给控制台,read将读取不到内容。 # -p 代表将read输入的参数 允许你在read命令行指定提示符 read -p "are you ok?" answer #测试STDIN是否恢复正常,如果不还原fd 0也就是STDIN,read就一直读取不到任何东西 case $answer in Y|y) echo "i am OK" ;; N|n) echo "sorrsy i am sad" ;; esac
6、exec创建读写文件描述符
shell会维护一个内部指针,指明在文件中的当前位置。任何读或写都会从文件指针上次的位置开始。
案例参考1
7、关闭exec创建的文件描述符
&- 关闭fd
一旦关闭了文件描述符,就不能在脚本中向它写入任何数据,否则shell会生成错误消息。
多个fd打开了同一个输出文件,后者覆盖前者的内容。
8、如何确定fd 已被使用
lsof命令会列出整个Linux系统打开的所有文件描述符。这是个有争议的功能,因为它会向非系统管理员用户提供Linux系统的信息。所以许多Linux系统隐藏了该命令。
我所使用的ECS实例的lsof在如下这个位置
lsof -a -p $$ -d 0,1,2,3,6
$$ 当前进程的PID
-p : PID
-d : FD
-a: 其他两个选项的结果执行布尔AND运算
[root@ninesun ~]# exec 6> file [root@ninesun ~]# [root@ninesun ~]# echo "hah" >&6 [root@ninesun ~]# cat file hah [root@ninesun ~]# [root@ninesun ~]# [root@ninesun ~]# lsof -a -p $$ -d 0,1,2,3,6 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME bash 1546070 root 0u CHR 136,0 0t0 3 /dev/pts/0 bash 1546070 root 1u CHR 136,0 0t0 3 /dev/pts/0 bash 1546070 root 2u CHR 136,0 0t0 3 /dev/pts/0 bash 1546070 root 6w REG 253,1 4 1052946 /root/file
STDIN、 STDOUT和STDERR关联的文件类型是字符型。因为STDIN、 STDOUT和STDERR文
件描述符都指向终端,所以输出文件的名称就是终端的设备名。 切可读可写
输出结果解释:
- COMMAND 正在运行的命令名的前9个字符
- PID 进程的PID
- USER 进程属主的登录名
- FD 文件描述符号以及访问类型( r代表读, w代表写, u代表读写)
- TYPE 文件的类型( CHR代表字符型, BLK代表块型, DIR代表目录, REG代表常规文件)
- DEVICE 设备的设备号(主设备号和从设备号)
- SIZE 如果有的话,表示文件的大小
- NODE 本地文件的节点号
- NAME 文件名