Perl IO:IO重定向

1,244 阅读7分钟

博客园的我:https://www.cnblogs.com/f-ck-need-u/

文件句柄和文件描述符的关系

文件描述符是操作系统的资源,对于实体文件来说,每打开一次文件,操作系统都会为该进程分配一个文件描述符来关联(指向)这个文件,以后操作文件数据都根据这个文件描述符来操作,而不是文件名。就像对文件句柄的操作一样。

实际上,文件句柄、文件描述符和实体文件的关系存在层次上的关系。文件句柄指向文件描述符,文件描述符指向实体文件结构。如下图:

正如图中所示,文件句柄是文件描述符的更上层封装,文件句柄指向文件描述符,且多个文件句柄还可以指向同一个文件描述符。同样地,多个文件描述符可以指向同一个实体文件。实际上,从文件到文件描述符,是采用引用计数的方式的,表示有多少个文件描述符还关联在这个文件上。同理,文件描述符到文件句柄,也是使用引用计数方式的,表示有多少文件句柄指向这个文件描述符

使用引用计数的特点之一就是只有引用数为0之后才表示关闭/删除/释放行为。例如,关闭文件句柄只是在文件描述符上引用数减一,而不是真的关闭文件描述符,直到文件描述符上的文件句柄引用数为0之后,这个文件描述符才会被关闭。同理,关闭文件描述符只是对文件结构的引用计数减1,直到这个文件结构的所有文件描述符都关闭了,才表示释放这个文件结构。

因为文件句柄是文件描述符的上层封装,所以文件句柄比文件描述符的功能多一些。实际上,从文件描述符到实体文件,中间的数据传输是纯裸数据流,不会有缓冲行为(当然,有非IO的缓冲)。而文件句柄到文件描述符,中间有好几个IO层次,例如编码层(utf8)、换行符层(raw/crlf)、标准IO库层(stdio/perlio)、最底层(unix)。如下图:

其中标准IO库层用来提供IO buffer层,stdio是操作系统提供的标准IO库,perlio是Perl提供的标准IO库,在Perl中可以选择使用哪种IO库提供buffer。unix是最底层(就算是在win下也是unix层),它是最接近文件描述符的底层,几乎是纯裸数据,没有IO buffer

模块PerlIO::Layers提供了Perl在文件描述符到文件句柄的IO层次上的一些检测功能,例如检测文件句柄是否已打开,是否设置了autoflush,是否使用缓冲等等。

文件句柄、文件描述符的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过程,结果如下图:

在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句柄的一个别名,使得它两指向同一个文件描述符。如下:

因为两个句柄指向同一个文件描述符,所以这两个文件句柄共享了这个文件描述符,包括这个描述符上的锁。另外,从任一句柄更改描述符状态,都会直接反映到另一个文件句柄上,比如从一个文件句柄上加一把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。