为什么父进程直到子进程退出才能看到子进程的输出?

7
请考虑以下脚本:
use IO::File;
$| = 1;
my ($handle, $pid) = myPipe();
if ($pid == 0) {
  print "$$";
  sleep 5;
  exit;
}

print "child: ".<$handle>."\n";

sub myPipe {
  my $handle = new IO::File();
  my $pid = open($handle, "-|");
  return ($handle, $pid);
}

在这种情况下,“child:”消息在进程启动后5秒钟才出现。如果我从forked child中删除sleep调用,则会立即打印。为什么forked child必须退出才能将数据刷新到父进程?

3个回答

13

有两个问题。首先,子进程正在缓冲其输出;其次,父进程正在使用<>操作符,该操作符会阻塞直到完整的一行可用或直到文件结束。

因此,获得您期望的结果的一种方法是让子进程在写入后立即关闭其输出流:

if ($pid == 0) {
    print "$$";
    close STDOUT;
    sleep 5;
    exit;
}

另一种方法是在子进程的输出中添加一个新行符,然后刷新流:

if ($pid == 0) {
    print "$$\n";
    STDOUT->flush;  # "close STDOUT;" will work too, of course
    sleep 5;
    exit;
}

必须进行刷新,因为管道通常是无缓冲的,而与终端连接的流通常是行缓冲的。

第三种选择是将子进程的输出流设置为自动刷新:

if ($pid == 0) {
    $| = 1;
    print "$$\n";
    sleep 5;
    exit;
}

感谢您的详细回答。我后来意识到使用<>需要一个新行字符。 - dromodel

3
清空管道的时间表不是固定的。刷新管道的唯二方法是退出子进程(这就是您现在正在做的),或显式调用flush。您可以通过以下任一操作使perl中的句柄清空:
  • 在子进程消息的末尾添加\n,通常会导致管道清空
  • $|设置为1,将自动刷新当前选择的文件句柄
  • 使用IO::Handle并调用$handle->flush
  • 使用IO::Handle并设置$handle->autoflush = 1

3
清空管道不是由内核处理的,缓存是用户空间的特性! - Leon Timmermans

3

在某些(大多数?)系统上,管道默认使用I/O缓冲。将

$handle->autoflush(1);

在你的myPipe函数中,需要加上语句。

但即使关闭缓冲区,Perl仍然不会在除换行符外的其他情况下刷新输出。因此,您可能还希望在子进程的输出中包含一个换行符。


更新:测试您的代码(Cygwin,perl 5.10.0,YMMV),我发现问题是子进程输出中缺少换行符,而不是在创建管道时是否显式启用了自动刷新。


1
这并不完全正确。如果自动刷新被打开,句柄是非缓冲的(每次打印后都会刷新),如果自动刷新被关闭并且句柄被打开到tty,则为行缓冲(每次换行后刷新),否则为块缓冲(通常在缓冲区满时刷新,大小通常为几KB)。 - hobbs
1
回复:更新--请参见Sean的答案。换行符并不重要,因为缓冲区,而是因为父进程中readline/<>操作需要在返回之前读取一个换行符,除了EOF :) - hobbs
@hobbs - 感谢您的澄清。在这个特定的例子中,父进程中对<$handle>的调用直到输入中有一个换行符或输入被关闭才会返回。所以换行符仍然是问题,但原因略有不同。我想你可以用$/来做一些奇怪的事情。 - mob

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