骏马金龙

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!
我本问道人,道心不坚,必将与道无缘!

Perl IO:操作系统层次的IO

sysopen()

open()和sysopen()都打开文件句柄,open()是比较高层次的打开文件句柄,sysopen()相对要底层一点。但它们打开的文件句柄并没有区别,只不过sysopen()有一些自己的特性:可以使用几个open()没有的flag,可以指定文件被创建时的权限等。

一定要注意的是,io buffer和open()、sysopen()无关,而是和读、写的方式有关,例如read()、getc()以及行读取都使用io buffer,而sysread、syswrite则直接绕过io buffer。

例如:

sysopen HANDLE, "file.txt", O_RDONLY;
open HANDLE, $filename, O_WRONLY|O_CREAT, 0644;
open HANDLE, $filename, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH;

其中sysopen支持的flag部分有以下几种:

# 三种基本读写模式
O_RDONLY
O_RDWR
O_WRONLY

# 配合基本读写模式的额外模式
O_APPEND
O_TRUNC
O_CREAT
O_EXCL       只能配合CREAT使用,只有文件
             不存在时才创建,文件存在时直
             接失败而不是打开它

O_BINARY     二进制模式,表示不做换行符转换
O_TEXT       文本模式,表示做换行符转换

O_NONBLOCK  非阻塞模式
O_NDELAY    同上,另一种表示方式,为了可移
            植性,建议使用上面的模式

非阻塞读写

sysopen()比open()多出的一个好用的特性就是O_NONBLOCK。在使用非阻塞的IO时,可以在等待过程中去执行其它任务。

但是需要明确一点,对于普通文件的读写,如果没有使用文件锁,那么没有必要使用O_NONBLOCK,因为内核缓冲区的存在使得对普通文件的读写不可能会出现阻塞问题。

例如:

use Fcntl;
open HANDLE, '/dev/ttyS0', O_RDONLY | O_NONBLOCK;

# sysread一个字符,但不会阻塞
my $key;
while(sysread HANDLE, $key, 1){
    if (defined $key){
        print "got $key\n";
    } else {
        do other tasks;
    }
    sleep 1;
}

close HANDLE;

O_NONBLOCK在无法成功返回时,将返回EAGAIN,并设置给$!变量。所以,更优写法如下:

use Fcntl;
open HANDLE, '/dev/ttyS0', O_RDONLY | O_NONBLOCK;

# sysread一个字符,但不会阻塞
my $key;
while(sysread HANDLE, $key, 1){
    if (defined $key){
        print "got $key\n";
    } else {
        if($! == 'EAGAIN'){
            do other tasks;
            sleep 1;
        } else {
            warn "Error read: $!";
            last;
        }
    }
}

close HANDLE;

IO::File自动探测(sys)open

使用IO::File模块的new()方法可以根据提供的参数方式自动探测要调用open()还是sysopen()来打开文件句柄:

  • 如果使用字符串的模式(< > >> +> +< +>>),则调用open()
  • 如果使用数值格式或O_XXX或使用了权限位,则调用sysopen()

例如:

# open()
my $fh = IO::File->new($filename, "+<");

# sysopen()
my $fh = IO::File->new($filename, O_RDWR);
my $fh = IO::File->new($file, O_RDWR, 0644);

sysread()

sysread()实现操作系统层的读操作,它通过read()系统调用直接读取文件描述符,会直接绕过IO Buffer层。

sysread FILEHANDLE,SCALAR,LENGTH,OFFSET
sysread FILEHANDLE,SCALAR,LENGTH

表示从FILEHANDLE文件句柄中读取LENGTH字节长度的数据保存到标量SCALAR中,如果指定了OFFSET,则从标量的OFFSET位置处开始写入读取到的数据。sysread()的返回值是读取到的字节数。

下面是一个结合O_NONBLOCK修饰符的sysread()。

#!/usr/bin/perl
use strict;
use warnings;
use Fcntl;

sysopen my $fh, $ARGV[0], O_RDONLY | O_NONBLOCK
    or die "open failed: $!";

my $data;
my $size = sysread $fh, $data, 20;
if ($size == 20 ){
    # 已读取20字节
    # 继续读取30字节追加到$data尾部
    $size += sysread $fh, $data, 30, 20;
    print "已读数据为: $data\n";
    if ($size < 50){
        print "文件大小为$size,数据不足50字节\n";
    }else{
        print "已读字节数:$size\n";
    }
} elsif($size > 0) {
    print "文件大小为$size,数据不足20字节\n";
} else {
    print "空文件\n";
}

在上面的代码中,主要是sysread()和O_NONBLOCK需要解释下。如果没有O_NONBLOCK,那么sysread()在读取不到20字节、50字节时将被阻塞等待。但现在的情况下,如果数据不足20、50字节时,sysread()将直接返回,并返回读取到的字节数(小于20或30)。

这里如果sysread()替换成read(),它们的区别是,sysread()只读取20或50字节数据,不会多读任何一个字节,而read()则可能多读一些数据到IO Buffer中,但是只取其中的20或50字节,剩余的数据遗留在IO Buffer中

于是,文件IO的指针就出现了不同值,对于sysread,他的指针就是20或50位置处,但是read()读取数据时,底层的文件指针将可能出现在1000字节处,但IO buffer中的IO指针将在20或50处。根据这个差值,可以计算出IO Buffer中保存了多少字节的数据。见后文sysseek()。

最后注意,不存在syseof()这样的函数来判断是否读取到了文件的结尾。但是,可以通过读取数据的返回值(即读了多少字节)来判断是否到了文件结尾

syswrite()

syswrite()实现操作系统层的写操作,它通过write()系统调用直接向文件描述符中写入数据,它会直接绕过IO Buffer层。

syswrite FILEHANDLE,SCALAR
syswrite FILEHANDLE,SCALAR,LENGTH
syswrite FILEHANDLE,SCALAR,LENGTH,OFFSET

如果只有两个参数,则直接将标量SCALAR代表的数据写入到FILEHANDLE中,如果指定了LENGTH,则从SCALAR中取LENGTH字节长度的数据写入到FILEHANDLE中,如果指定了OFFSET,则表示从标量的OFFSET处开始截取LENGTH长度的数据写入到FILEHANDLE中。OFFSET可以指定为负数,这表示从尾部开始数写入OFFSET个字节的数据。如果LENGTH大于从OFFSET开始计算可以获取到的数据,则能获取到多少数据就写入多少数据。

syswrite()返回实际写入的字节数,如果出错了则返回undef。

例如:

# 写入abcdef
syswrite STDOUT, "abcdef";

# 写入abc
syswrite STDOUT, "abcdef", 3;

# 写入cde
syswrite STDOUT, "abcdef", 3, 2;

# 写入cde
syswrite STDOUT, "abcdef", 3, -4;

实际上,不适用syswrite(),直接使用print也可以绕过io buffer,但前提是设置文件句柄的IO层为unix层,因为unix层是文件句柄到文件描述符的最底层,它会禁用所有上层,包括buffer。

binmod(FILEHANDLE, ":unix");

例如下面的例子中,在10秒内将每秒输出一个点,如果把binmode那行删除,将在10秒之后一次性输出10个点。如果删除binmode,还可以将print "."改为syswrite STDOUT, ".";,它将同样每秒输出一个点。

#!/usr/bin/perl

binmode(STDOUT, ":unix");
for (0..9){
    print ".";   # syswrite STDOUT, ".";
    sleep 1;
}
print "\n";

sysseek()

sysseek FILEHANDLE,POSITION,WHENCE

sysseek()通过lseek()系统调用设置或返回IO指针的位置,它直接绕过IO Buffer操作文件描述符。它和seek()的语法上没什么区别:

# seek using whence numbers
sysseek HANDLE, 0, 0; # rewind to start
sysseek HANDLE, 0, 2; # seek to end of file
sysseek HANDLE, 20, 1; # seek forward 20 characters

# seek using Fcntl symbols
use Fcntl qw(:seek);
sysseek HANDLE, 0, SEEK_SET; # rewind to start
sysseek HANDLE, 0, SEEK_END; # seek to end of file
sysseek HANDLE, 20, SEEK_CUR; # seek forward 20 characters

sysseek()返回设置IO指针位置后的新位置。例如原来IO指针位置为第三个字节处,向前移动5字节后,sysseek()将返回8。所以,可以通过sysseek()来实现tell()函数的功能,只需将sysseek()相对于当前位置移动0字节即可:

sysseek(HANDLE, 0, SEEK_CUR);

除了绕过IO buffer,sysseek()和seek()基本相同。但正是它绕过了io buffer,导致了sysseek()和tell()的结果可能大不一样,tell()获取的是IO Buffer中IO指针的位置,而sysseek(HANDLE, 0, SEEK_CUR)获取的是文件描述符层次的IO指针位置。所以在使用IO Buffer类的读函数时,可以通过sysseek() - tell()计算出缓冲在IO Buffer中的数据比缓冲在page cache中的数据少多少字节。这种额外缓冲一些数据到page cache的行为称为"预读"(readahead()),它带来的好处是可能会减少后续读操作阻塞的时间。

下面是一个说明tell()和sysseek()区别的示例:

#!/usr/bin/perl
use strict;
use warnings;

use 5.010;
use Fcntl q(:seek);

open my $fh, "<", "abc.log";

my $readed;
read $fh, $readed, 5;

print "tell pos: ", tell($fh);
print "sysseek pos: ", sysseek($fh, 0, SEEK_CUR);

向abc.log中写入10个字节(实际为11个字节,因为echo自动加换行符)的数据:

$ echo "0123456789" >abc.log

执行上面的Perl程序:

tell pos: 5
sysseek pos: 11

上面的程序中,使用read()函数读取了5个字节数据,但是read()是使用IO Buffer的,它会从磁盘文件中多读取一些数据放到page cache中,例如这里最多只能读11字节到page cache,然后从page cache中读取指定数量5字节的数据到IO buffer中供read读取并保存到$readed标量中。因为文件描述符已经读取了11个字节的数据,所以sysseek()的返回值为11,而tell()则是io buffer中读取数据的位置,即5字节。所以,read()结束后,page cache中还剩下6字节的数据供后续读操作放入到io buffer中

IO::Handle:sync()

从perl的文件句柄到操作系统的文件描述符,再从文件描述符到设备上的实体文件,两个层次之间都有各自的buffer层。

+--------------------------+
|        FileHandle        |
+--------------------------+
  |
  | Perl IO Buffer
  v
+--------------------------+
|     File Description     |
+--------------------------+
  |
  | page cache/buffer cache
  v
+--------------------------+
|        (dev)disk         |
+--------------------------+

其中文件描述符是操作系统的资源,从文件描述符到硬件设备之间的缓冲(page cache),可以通过fsync()来刷,但Perl文件句柄层的IO Buffer,操作系统显然不负责这个属于Perl IO的上层缓冲。

Perl的IO::Handle中提供了刷缓冲的方法:

  • flush():将Perl IO Buffer中的数据刷到文件描述符
    • IO Buffer中未读取完的数据被丢弃
    • IO Buffer中未写入完成的数据立即写入到文件描述符,写入完成后flush才成功
  • sync():实现和系统调用fsync()相同的功能,将文件描述符到设备文件之间的缓冲刷盘
posted @ 2019-03-02 09:42 骏马金龙 阅读(...) 评论(...) 编辑 收藏