如何在Perl中刷新文件?

24

我有一个Perl脚本,它每三秒向现有文件追加一行内容。同时,有一个C++应用程序会从该文件中读取。

问题在于,应用程序在脚本完成并关闭文件句柄后开始读取文件。为了避免这种情况,我想在每次追加新行后进行强制刷新。如何实现?


在“基本”Perl中没有刷新函数,但是如果您调用binmode $filehandle,它将设置:raw格式,并(作为副作用)进行刷新。它的效果很好。无论如何,如果它在Unix上,您可以创建一个命名管道到文件系统,然后直接写入它。 - TrueY
让你的C++程序从管道(/dev/stdin如果需要文件名而愚蠢)中读取,并检查它是否能够逐行读取 - 如果使用块缓冲读取,则在写入端无法进行任何操作。 - reinierpost
11个回答

32

尝试:

use IO::Handle;
$fh->autoflush;

实际上,这是我早期提出的自动刷新方法之一(我的一个问题),我当时询问了关于普遍接受的不好的实现方式 :-)


1
谢谢。但这并没有帮助我。即使在每行之后刷新,我的C++程序仍然无法读取这些行,在它们被插入之后。只有在perl完成其工作后,它才能读取。 - Mihran Hovsepyan
现在你如何刷新STDOUT? - Chloe
1
@Chloe 使用 $| = 1 - talkloud

30

TL/DR: 使用 IO::Handleflush 方法,例如:

use IO::Handle;
$myfile->flush();

首先,您需要决定要多么“刷新”。可以有相当多的缓冲层:
  • Perl文件句柄上的内部缓冲区。其他程序在数据离开此缓冲区之前无法看到数据。

  • “脏”文件块的文件系统级缓冲。其他程序仍然可以看到这些更改,它们似乎已经“写入”,但如果操作系统或计算机崩溃,它们将丢失。

  • 写入的磁盘级回写缓冲。操作系统认为这些已写入磁盘,但实际上磁盘只是将它们存储在驱动器上的易失性内存中。如果操作系统崩溃,数据不会丢失,但如果电源失败,则可能会丢失,除非磁盘可以首先将其写出。这是廉价消费者SSD的一个大问题。

当涉及SAN、远程文件系统、RAID控制器等时,情况变得更加复杂。如果通过管道进行写入,还需要考虑管道缓冲区。

如果您只想清空Perl缓冲区,您可以关闭文件,打印一个包含"\n"的字符串(因为似乎Perl在换行时刷新),或者使用IO::Handleflush方法
您还可以像perl faq中所述,使用binmode或操作$|使文件句柄无缓冲。这与清空缓冲句柄不同,因为将一堆缓冲写入队列,然后执行单个刷新的性能成本比写入无缓冲句柄要低得多。
如果您想刷新文件系统写回缓冲区,您需要使用像fsync()这样的系统调用,以O_DATASYNC模式打开文件,或使用众多其他选项。这是非常复杂的,正如PostgreSQL拥有自己的工具来测试文件同步方法所证明的那样。
如果您想确保它真正、真实、诚实地存储在硬盘上的永久存储中,您必须在程序中将其刷新到文件系统。您还需要配置硬盘/SSD/RAID控制器/SAN/任何其他设备,以便在操作系统要求时真正刷新。这可能会非常复杂,并且与操作系统/硬件有关。强烈建议进行“插拔”测试,以确保您已经正确地完成了这项任务。

19

来自《Perl常见问题解答》手册:

$old_fh = select(OUTPUT_HANDLE);
$| = 1;
select($old_fh);

如果你只想清空标准输出(stdout),你可以尝试这样做:

$| = 1;

但是请查看FAQ以了解一个模块,它可以为您提供更易于使用的抽象,比如IO::Handle


另一种(复杂的,单行)编写方式是 select((select(OUTPUT_HANDLE),$|=1)[0])(摘自 https://dev59.com/gHVC5IYBdhLWcg3wvT7g )。请参见该线程中有关此方法的其他考虑、优点和问题。 - msb

4

这里是答案 - 真正的答案。

停止在进程的整个生命周期中保持此文件的打开文件句柄。

开始将您的文件追加操作抽象为一个子程序,该子程序以追加模式打开文件,写入内容并关闭文件。

# Appends a new line to the existing file
sub append_new_line{
    my $linedata = shift;
    open my $fh, '>>', $fnm or die $!; # $fnm is file-lexical or something
    print $fh $linedata,"\n"; # Flavor to taste
    close $fh;
}

观察文件的过程会遇到一个关闭的文件,每当调用该函数时它都会被修改。

2
没错,它有效。我今天在 Perl 脚本中使用了这个原理。性能不是问题——每 16 秒写一行代码。 - Peter Mortensen
也许可以在特定条件下进行实验,测量开销? - Peter Mortensen
在Perl中,我们优化程序员的时间,除了事情变成二次方之外。 - Never Sleep Again
这种方法只适用于文件很少更新的情况。但是对于频繁更新的情况,这是一种错误的方式。否则,您将使用某些文本文件作为日志。所有行都以“/n”结尾,然后按行自动刷新。然后我们同意,文件不应该被多次关闭/打开。但是.......有时日志文件非常大。那么您必须执行一些程序来进行日志轮换。第一种方法是检查文件inode是否相同。如果不同,在写入之前必须进行关闭/打开。否则,您必须检查来自日志轮换的信号反应,以切换到另一个文件。 - Znik

2
所有建议设置自动刷新的解决方案都忽略了一个基本事实,即现代大多数操作系统都会缓存文件I/O,无论Perl正在做什么。
你唯一强制将数据提交到磁盘的可能性是关闭文件。
我现在陷入了同样的困境,我们遇到了一个正在写入日志的旋转问题。

6
你只有通过关闭文件来强制将数据写入磁盘的可能性。关闭文件并不会将其写入磁盘,操作系统仍然可以将其缓存起来以便写回。唯一保证的方法是调用fsync()或你的操作系统等价的文件描述符。否则,在操作系统崩溃或断电时,数据可能会丢失。 - Craig Ringer

1

1

为了自动刷新输出,您可以像其他人描述的那样在输出到文件句柄之前设置autoflush/$|

如果您已经输出到文件句柄并且需要确保它到达物理文件,则需要使用IO::Handle flushsync方法。


0

真正正确的答案是使用:

$|=1; # Make STDOUT immediate (non-buffered)

虽然这是你问题的一个原因,但同样的问题的另一个原因是:“还有一个C++应用程序从那个文件中读取。”

编写能够正确读取正在增长的文件的C++代码非常复杂,因为当你的“C++”程序到达文件末尾时,它会遇到EOF...(你不能在没有严重额外技巧的情况下读取文件的结尾) - 你必须使用IO阻塞和标志进行一堆复杂的操作来正确监视文件(就像Linux的“tail”命令的工作方式一样)。


0

对于那些正在寻找一种解决方案,以便在使用会话文件(*.cse)时将输出逐行刷新到文件中的Ansys CFD Post,这是我唯一有效的解决方案:

! $file="Test.csv";
! open(OUT,"+>>$file");
! select(OUT);$|=1;  # This is the important line
! for($i=0;$i<=10;$i++)
! {
!    print out "$i\n";
!    sleep(3);
! }

请注意,在包含 Perl 脚本的每一行开头都需要叹号标记。 sleep(3); 仅用于演示目的。use IO::Handle; 不需要。

0
另一种方法是使用命名管道来替代你目前正在使用的文件,在Perl脚本和C++程序之间进行通信。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接