Perl IO:IO重定向
文件句柄和文件描述符的关系
文件描述符是操作系统的资源,对于实体文件来说,每打开一次文件,操作系统都会为该进程分配一个文件描述符来关联(指向)这个文件,以后操作文件数据都根据这个文件描述符来操作,而不是文件名。就像对文件句柄的操作一样。
实际上,文件句柄、文件描述符和实体文件的关系存在层次上的关系。文件句柄指向文件描述符,文件描述符指向实体文件结构。如下图:
(图注:fd是用户空间的内容,图中放在内核层是为了概括与之关联的内核层的几个结构:fd对应内核层的这几个结构)
正如图中所示,文件句柄是文件描述符的更上层封装,文件句柄指向文件描述符,且多个文件句柄还可以指向同一个文件描述符。同样地,多个文件描述符可以指向同一个实体文件。实际上,从文件到文件描述符,是采用引用计数的方式的,表示有多少个文件描述符还关联在这个文件上。同理,文件描述符到文件句柄,也是使用引用计数方式的,表示有多少文件句柄指向这个文件描述符。
使用引用计数的特点之一就是只有引用数为0之后才表示关闭/删除/释放行为。例如,关闭文件句柄只是在文件描述符上引用数减一,而不是真的关闭文件描述符,直到文件描述符上的文件句柄引用数为0之后,这个文件描述符才会被关闭。同理,关闭文件描述符只是对文件结构的引用计数减1,直到这个文件结构的所有文件描述符都关闭了,才表示释放这个文件结构。
因为文件句柄是文件描述符的上层封装,所以文件句柄比文件描述符的功能多一些。实际上,从文件描述符到实体文件,中间的数据传输是纯裸数据流,不会有缓冲行为(当然,从操作系统的角度上看,有文件系统层的IO缓冲或操作系统层的缓冲,如page cache、buffer cache)。而文件句柄到文件描述符,中间有好几个IO层次,例如编码层(utf8)、换行符层(raw/crlf)、标准IO库层(stdio/perlio)、最底层(unix)。如下图:
(图注:fd是用户空间的内容,图中放在内核层是为了概括与之关联的内核层的几个结构:fd对应内核层的这几个结构。但page cache是内核空间的)
其中标准IO库层用来提供IO buffer层,stdio是操作系统提供的标准IO库,perlio是Perl提供的标准IO库,在Perl中可以选择使用哪种IO库提供buffer。unix是perl io的最底层(就算是在win下也是unix层),它几乎等价于直接操作文件描述符,相当于是纯裸数据,没有IO buffer。
模块PerlIO::Layers提供了Perl在文件描述符到文件句柄的IO层次上的一些检测功能,例如检测文件句柄是否已打开,是否设置了autoflush,是否使用缓冲等等。
另外,从文件句柄到文件描述符中间的IO Buffer中的数据,可以通过perl IO::Handle->flush()
来刷,flush时,io buffer中未读数据被丢弃,未写数据将写入到文件描述符。从文件描述符到设备文件(如磁盘上的abc.log)中间的缓冲(如page cache),可以通过操作系统的fsync()系统调用或perl IO::Handle->sync()
来刷盘。
文件句柄、文件描述符的duplicate
在bash shell中经常见到的>file 2>&1
,它表示将标准错误、标准输出都重定向到file文件中。这里的过程是将标准输入重定向到file文件,然后duplicate文件描述符fd=1得到fd=2,使得fd=2也指向fd=1对应的文件(即file),从而使得标准错误、标准输出都输出到file中。
除了重定向、文件描述符的duplicate,bash shell还支持文件描述符的手动打开(分配文件描述符)、移动、关闭。
Perl当然也支持类似的重定向和duplcate,而且不仅支持文件描述符级别的,还支持更上层别文件句柄级,无论是duplicate文件句柄还是duplicate文件描述符,都会生成新的文件描述符。另外,duplicate的对象是文件句柄时,不会将IO Buffer中的内容也duplicate,也就是说新的文件句柄中没有缓冲任何数据。
在Perl中,可以在open时在>、>>、<、+>、+>>、+<
的后面加上符号&
,这就表示文件句柄或文件描述符的duplicate。给文件句柄就是文件句柄的duplicate,给数值就是文件描述符的duplicate。open可以是两参数的或三参数的,三参数时,可以是文件句柄、文件句柄的引用(即\*FILEHANDLE
格式),可以是文件描述符数值。如果需要获取文件句柄指向的文件描述符,可以使用fileno FILEHANDLE
函数来获取。
例如,下面将STDOUT文件句柄duplicate一份得到NEWOUT,使得NEWOUT也指向标准输出,即向NEWOUT写入数据时也会出现在屏幕上(默认)。
# 两参数或三参数的文件句柄duplicate
open NEWOUT, ">&STDOUT";
open NEWOUT, ">&", "STDOUT";
open NEWOUT, ">&", "\*STDOUT";
# 三参数的文件描述符duplicate
open NEWOUT, ">&", fileno STDOUT;
按照上面的duplicate过程,结果如下图:
(图注:fd是用户空间的内容,图中放在内核层是为了概括与之关联的内核层的几个结构:fd对应内核层的这几个结构)
在duplicate时,所选的模式一定要匹配源文件句柄的模式。例如STDOUT是可写不可读(write-only)的文件句柄,在duplicate STDOUT时,就必须只能选择可写不可读的>&
模式。duplicate后,新的文件句柄或文件描述符和源文件句柄/文件描述符的读、写模式是完全一样的。
下面是将STDOUT复制多份的示例:
#!/usr/bin/perl
#
use strict;
use warnings;
use 5.010;
open NEWOUT, ">&STDOUT" or die "duplicate1 failed: $!";
say NEWOUT "hello world1, fd=", fileno NEWOUT;
open NEWOUT1, ">&", "NEWOUT" or die "duplicate2 failed: $!";
say NEWOUT1 "hello world2, fd=", fileno NEWOUT1;
open NEWOUT2, ">&", "\*NEWOUT" or die "duplicate3 failed: $!";
say NEWOUT2 "hello world3, fd=", fileno NEWOUT2;
open NEWOUT3, ">&", fileno NEWOUT or die "duplicate4 failed: $!";
say NEWOUT3 "hello world4, fd=", fileno NEWOUT3;
close NEWOUT;
close NEWOUT1;
close NEWOUT2;
close NEWOUT3;
执行后输出结果:
hello world1, fd=3
hello world2, fd=4
hello world3, fd=5
hello world4, fd=6
文件描述符重用:句柄别名
duplicate文件句柄或文件描述符时,都会自动新建一个新的文件描述符,并自动新建指向这个文件描述符的文件句柄。也就是说,只要duplicate一次,就至少有2个描述符,两个句柄。
如果想要重用文件描述符,只新建文件句柄,Perl中可以使用&=
符号(<&=、>&=、>>&=、+<&=、+>&=、+>>&=
),这表示创建一个文件句柄别名,使这个文件句柄也指向同一个文件描述符。也支持直接对文件描述符设置别名句柄,它会新建一个句柄指向这个文件描述符。
例如:
open(ALIAS, ">&=HANDLE");
open ALIAS, ">&=", fileno HANDLE;
这表示创建HANDLE句柄的一个别名,使得它两指向同一个文件描述符。如下:
(图注:fd是用户空间的内容,图中放在内核层是为了概括与之关联的内核层的几个结构:fd对应内核层的这几个结构)
因为两个句柄指向同一个文件描述符,所以这两个文件句柄共享了这个文件描述符,包括这个描述符上的锁。另外,从任一句柄更改描述符状态,都会直接反映到另一个文件句柄上,比如从一个文件句柄上加一把flock锁,因为flock锁是直接文件描述符上的,所以另一个文件句柄别名也会持有这把锁。
重定向
在bash中重定向非常的简单,在Perl中重定向直接使用> < >> +> +>> +<
即可,只不过open的第一个参数是一个已存在的文件句柄,其无非是将输入自或输出到的某个文件句柄/文件描述符的数据转向另一个方向。
例如,将输出到标准输出的数据重定向到某个文件中,就像使用select FILEHANDLE
一样。
open STDOUT, "> abc.log";
say "hello world"; # 将输出到abc.log文件中
再例如输入重定向,STDIN本该是从标准输入中读取数据的,但是现在改从一个文件中读取数据。
open STDIN, "< abc.log";
while(<STDIN>){
chomp;
print "$_\n";
}
但是这样使用重定向功能会有一个问题,STDOUT或STDIN或其它重定向句柄没法还原回原始的目标了。例如STDOUT原本是输出到终端的,将其重定向到某个文件后就没法找回输出到终端的方法了。所以,在程序中使用重定向时,经常会将重定向配合duplicate使用,在重定向之前,先将重定向句柄dup保存一份,然后重定向,重定向结束后再使用保存的句柄恢复回来。
例如:
# 1.dup。OLDOUT和STDOUT都将指向同一个底层文件结构:终端设备文件
open OLDOUT, ">&", STDOUT or die "duplicate failed: $!";
# 2.redirect。OLDOUT仍然指向终端设备文件,但是STDOUT指向新文件结构
open STDOUT, "> $newfile" or die "redirect failed: $!";
... do something to STDOUT ...
# 3.restore。通过dup的方式从OLDOUT恢复STDOUT
close STDOUT or die "close failed: $!";
open STDOUT, ">&", OLDOUT or die "duplicate failed: $!";
注意上面第三步中恢复之前,记得先关闭STDOUT,如果不关闭STDOUT,在第二步过程中STDOUT中的缓冲不会flush。
Linux系列文章:https://www.cnblogs.com/f-ck-need-u/p/7048359.html
Shell系列文章:https://www.cnblogs.com/f-ck-need-u/p/7048359.html
网站架构系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html
MySQL/MariaDB系列文章:https://www.cnblogs.com/f-ck-need-u/p/7586194.html
Perl系列:https://www.cnblogs.com/f-ck-need-u/p/9512185.html
Go系列:https://www.cnblogs.com/f-ck-need-u/p/9832538.html
Python系列:https://www.cnblogs.com/f-ck-need-u/p/9832640.html
Ruby系列:https://www.cnblogs.com/f-ck-need-u/p/10805545.html
操作系统系列:https://www.cnblogs.com/f-ck-need-u/p/10481466.html
精通awk系列:https://www.cnblogs.com/f-ck-need-u/p/12688355.html