随机读写
如果一个文件句柄是指向一个实体文件的, 那么就可以对它进行随机数据的访问(包括随机读, 写), 随机访问表示可以读取文件中的任何一部分数据或者向文件中的任何一个位置处写入数据. 实现这种随机读写的功能依赖于一个文件读写位置指针(file pointer).
当一个文件句柄关联到了一个实体文件后, 就可以操作这个文件句柄, 比如通过这个文件句柄去移动文件的读写指针, 当这个指针指向第 100 个字节位置处时, 就表示从 100 个字节处开始读数据, 或者从 100 个字节处开始写数据.
可以通过 seek()函数来设置读写指针的位置, 通过 tell()函数来获取文件读写指针的位置. 如果愿意的话, IO::Seekable 模块同样提供了一些等价的面向对象的操作方法, 不过它们只是对 seek,tell 的一些封装, 用法和工作机制是完全一样的.
需要注意的是, 虽说文件读写指针不是属于文件句柄的, 但通过文件句柄去操作读写指针的时候, 可以认为指针是属于句柄的. 例如, 同一个文件的多个文件句柄的读写指针是独立的, 如果文件 A 上同时打开了 A1 和 A2 两个文件句柄, 那么在 A1 上设置的读写指针不会影响 A2 句柄的读写指针.
seek 跳转文件指针位置
通过 seek()函数, 可以让文件的指针随意转到哪个位置. 注意还有一个 sysseek()函数, 它们不同, seek()是工作在 buffer 上的, sysseek()是底层无 buffer 的.
seek FILEHANDLE, POSITION, WHENCE
seek()有三个参数:
第一个参数是文件句柄
第二个参数是正负整数或 0, 它的意义由第三个参数决定
第三个参数是 flag, 用来表明相对与哪个位置进行跳转的, 值可以是 0,1 和 2. 如果导入了 Fcntl 模块的 seek 标签(即
use Fcntl qw(:seek)
), 则可以使用 0,1,2 对应的常量 SEEK_SET,SEEK_CUR,SEEK_END 来替代 0,1,2
第三个参数的值决定第二个参数的意义. 如下:
seek 意义
-----------------------------------------------------------------
seek FH, $pos, 0 以相对于文件开头的位置跳转 $pos 个字节,
seek FH, $pos, SEEK_SET 即直接按照绝对位置的方式设置指针位置.
pos 不能是负数, 否则表示跳转到文件头的
前面, 这会使得 seek 失败而返回 0. 例如
`seek FH, 0, 0` 表示跳转到开头(第一个
字节前),`seek FH, 10, 0` 表示跳转到第
10 字节前
seek FH, $pos, 1 以相对于当前指针的位置向前(pos 为正数),
seek FH, $pos, SEEK_CUR 向后 (pos 为负数) 跳转 pos 个字节, pos=0 表
示保持原地不动. 例如 `seek FH, 60, 1`
表示指针向右 (向前) 移动 60 个字节. 如果移
动到超出文件的位置并从这里写入数据, 将
以空字节 `\0` 填充直到那个位置
seek FH, $pos, 2 以相对于文件尾部的位置跳转 $pos 个字节.
seek FH, $pos, SEEK_END 如果 pos 为负数, 表示向文件头方向移动 pos
个字节, 如果 pos 为 0 则表示保持在尾部不动,
如果 pos 大于 0 且要写入, 则会以空字节 `\0`
填充直到那个位置. 例如 `seek FH, -60, 2`
表示从文件尾部向文件头部移动 60 个字节
seek 在成功跳转成功时返回 true, 否则返回 0 值, 例如想要跳转到文件头的前面, 这时返回 0 且将指针放置到文件的结尾.
比如用 seek 来创建一个大文件:
- open BIGFILE, ">", "bigfile.txt";
- seek BIGFILE, 100*1024, 0; # 100K
- syswrite BIGFILE, 'endendend' # 100k + 9bytes
- close BIGFILE
跳转超出文件尾部后, 如果要真的让文件扩充, 需要在结尾的地方写入一点数据, 否则不会填充. 这就相当于用类似于下面的 dd 命令创建一个稀疏大文件一样.
dd if=/dev/zero of=bigfile seek=100 count=1 bs=1K
tell()函数获取文件指针位置
tell FILEHANDLE
tell 函数获取给定文件句柄当前文件指针的位置.
唯一需要注意的一点是, 如果文件句柄指向的文件描述符不是一个实体文件, 比如套接字句柄, tell 将返回 - 1. 注意不是返回 undef, 尽管我们可能更期待它返回 undef 来判断.
- $pos = tell MYHANDLE;
- print "POS is", $pos> -1 ? $pos : "not file", "\n";
IO::Seekable
IO::Seekable 模块提供了 seek 和 tell 的封装方法. 例如:
- $fh->seek($pos, 0); # SEEK_SET
- $fh->seek($pos, SEEK_CUR);
- $pos = $fh->tell();
seek 在 EOF 处读
就像实现 tail -f 一样监控每秒写入到文件尾部的数据并输出. 如果使用 seek 来实现这个功能的话, 参考如下:
- #!/usr/bin/perl
- use strict;
- use warnings;
- die "give me a file" unless(@ARGV and -f $ARGV[0])
- open my $taillog, $ARGV[0];
- while(1){
- while(<$tailog>){print "$.: $_";}
- seek $taillog, 0, 1;
- sleep 1;
- }
上面的程序中, 先读取出文件中的数据, 然后将文件的指针保持在原地以便下次循环继续从这里开始读取, 睡一秒后继续, 这个逻辑并不难.
当然, 对于上面简单的 tail -f 来说, 根本没使用 seek 的必要, 但是这提供了一种连续从尾部读取数据的思路.
seek 在 EOF 处写
典型的是写日志文件, 要不断地向文件尾部追加一行行日志数据. 但是, 多个进程可能会互相覆盖数据, 因为不同进程的写真正是互相独立的, 谁也不知道谁的指针在哪里. 如果使用的是追加式写入方式, 则多进程间不会出现数据覆盖的问题, 因为每次 append 数据之前都会将指针放到文件的最结尾处. 但是多个进程的 append 无法保证每行数据写入的顺序.
如果要保证某进程某次两行数据的写入是紧连在一起的, 那么需要使用锁的方式, 例如使用 flock 文件锁.
下面是一个简单的日志写入程序示例:
- #!/usr/bin/perl
- use strict;
- use warnings;
- use Fcntl qw(:flock :seek);
- sub logtofile {
- die "give me two args" if @_ <1;
- my $logfile = shift;
- my @msg = @_;
- open LOGFILE, ">>", $logfile or die "open failed: $!";
- flock LOGFILE, LOCK_EX;
- seek LOGFILE, 0, SEEK_END;
- print LOGFILE @msg;
- close LOGFILE;
- }
- logtofile "/tmp/mylog.log", "msgA\n", "msgB\n", "msgC\n";
truncate 截断文件
如果要截断文件为某个空间大小, 直接使用 truncate()函数即可(shell 下也有 truncate 命令来截断文件).
它的第一个参数是文件句柄, 第二个参数是截断后的文件大小, 单位字节. 注意, truncate 是从当前指针位置开始向后截断的, 其指针前面 (左边) 的数据不会动但是会计算到截断后的大小. 如果指定的截断大小超过文件大小, 则会使用空字节 \ 0 填充到给定大小(这个行为默认没有定义).
因为要截断, 这个文件句柄的模式必须是可写的, 且如果是使用 ">" 模式, 将首先被截断为空文件. 所以, 应该使用 +<,>>,+>>这类模式. 为了保证截断效果, 如果使用的是后两种 open 模式, 应该在每次截断前使用 "seek" 将指针指到文件的头部.
例如, 截断文件为 100 字节大小.
- open FILE, ">>", "bigfile";
- seek FILE, 0, 0;
- truncate FILE, 100;
- close FILE;
按行截断文件
truncate 只能按字节截断文件, 不过有时候我们想按照行数来截断文件.
例如, 想要保留前 10 行数据. 实现的逻辑很简单, 先按行读取 10 行(判断行号或使用一个行号计数器), 然后记录下当前的指针位置, 最后使用 truncate 截断到这个位置.
- #!/usr/bin/perl
- use strict;
- use warnings;
- die "give me a file" unless @ARGV;
- die "give me a line num" unless (defined($ARGV[1]) and $ARGV[1]>= 0);
- my $file = $ARGV[0];
- my $trunc_to = int($ARGV[1]);
- # 读取到前 X 行
- open READ, $file or die "open failed: $!";
- while(<READ>){
- last if $. == $trunc_to;
- }
- my $trunc_size = tell READ;
- exit if $. < $trunc_to; # total line Less than $trunc_to
- close READ;
- # truncate
- open WRITE, "+<", $file or die "open failed: $!";
- truncate WRITE, $trunc_size or die "truncate failed: $!";
- close WRITE;
来源: https://juejin.im/post/5c7673b4f265da2daf79b34e