Perl 行为差异关闭由 open() 产生的子进程与 IPC::Open3

Posted

技术标签:

【中文标题】Perl 行为差异关闭由 open() 产生的子进程与 IPC::Open3【英文标题】:Perl Behavioral Differences Closing Child Process Spawned with open() vs. IPC::Open3 【发布时间】:2019-09-04 07:13:48 【问题描述】:

我正在尝试解决这个问题,但无法理解它。我需要打开一个管道子进程并从其输出中读取。最初我使用的是这样的标准 open() 调用:

#!/usr/bin/perl;

use warnings;
use strict;
use Scalar::Util qw(openhandle);
use IPC::Open3;

my $fname = "/var/log/file.log.1.gz";
my $pid = open(my $fh, "-|:encoding(UTF-8)", "gunzip -c \"$fname\" | tac");

# Read one line from the file
while (my $row = <$fh>) 
    print "Row: $row\n";
    last; # Bail out early


# Check if the PID is valid and kill it if so
if (kill(0, $pid) == 1) 
    kill(15, $pid);
    waitpid($pid, 0);
    $pid = 0;


# Close the filehandle if it is still open
if (openhandle($fh)) 
    close $fh;

上述方法有效,除了我在日志中收到来自 tac 的错误:

tac: write error

从我所做的各种测试和研究中可以看出,这是因为杀死从 open() 返回的 PID 只会杀死第一个子进程(但不是第二个),所以当我关闭文件句柄,tac 仍在写入它,因此由于管道损坏而导致“写入错误”。奇怪的是,有时当我检查 ($? >> 8) 时,如果 close() 调用返回 false,它将返回 141,表明它收到了 SIGPIPE(支持我上面的理论)。但是,有时它返回 0,这很奇怪。

此外,如果我运行相同的命令但没有双管道(只有一个),就像这样(其他一切都与上面相同):

my $pid = open(my $fh, "-|:encoding(UTF-8)", "gunzip -c \"$fname\"");

...我会在日志中收到如下错误:

gzip: stdout: Broken pipe

...但在这种情况下,gunzip/gzip 是唯一的进程(我通过返回的 PID 杀死了它),所以我不确定为什么当我关闭文件句柄时它仍会写入管道(因为它应该已经被杀死了,并且用 waitpid()) 等待。

我正在尝试在 Perl 调试器中重现这一点,但这很困难,因为我无法使用普通的 open() 获得子进程的 stderr(我在 prod 中看到外部进程的 stderr 的方式是apache2 日志 - 这是一个 CGI 脚本)。

我从阅读文档中了解到,我无法在使用 open() 的多管道打开中获取所有子进程的 PID,因此我决定尝试使用不同的方法,以便我可以关闭所有进程干净地。我尝试了 open3(),有趣的是,没有进行任何更改(实际上运行的场景与上面基本相同,但使用 open3() 而不是 open()):

my $pid = open3(my $in, my $fh, undef, "gunzip -c \"$fname\"");

...然后像我上面那样杀死它,我没有收到任何错误。这适用于如上所示的单管道流程,以及涉及管道到“tac”的双管道流程。

因此,我想知道我在这里缺少什么?我知道 open() 和 open3() 的工作方式存在差异,但是从它们产生子进程的方式是否存在差异?在这两种情况下,我都可以看到初始子进程(返回的 PID)本身就是 Perl 进程的子进程。但它几乎好像由 open() 产生的进程没有被正确杀死和/或清理(通过 waitpid()),而由 open3() 产生的同一进程是,这就是我无法弄清楚的部分.

而且,对于更大的图景和手头的问题 - 在这种情况下干净地关闭多管道进程的最佳方法的建议是什么?我在这方面花费的时间是否超过了应有的时间?除了这些错误之外,脚本本身可以正常工作,所以如果事实证明我看到的 tac 和 gzip 错误无关紧要,我应该忍受它们并继续前进吗?

非常感谢任何帮助!

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 @zdim - 关于你之前关于一个 open() 方法如何调用 shell 进程而另一个不调用的观点 - 我认为这可能正是导致不同行为的原因!我只是仔细观察了一下,确实 open() 总是调用一个 shell(在我的例子中是 sh -c gunzip -c [file],它是 Perl 进程本身的一个子进程,并且在它下面有一个 gzip -d -c [file] 的子进程),而 open3() 没有(当调用单管道命令时,它具有gzip -d -c [file] 作为 Perl 进程的直接子进程)。使用双管道,两种变体都会调用一个 shell。 正如@zdim 所说,它通过shell 运行命令,因为perl 注意到它包含shell 元字符(双引号)。您可以终止 shell 进程及其所有子进程(进程组)传递给 kill 被否定的 pid 号,如 kill 15, -$pid 另外,您不需要在创建为管道的进程上waitpid。无论如何,Perl 都会通过 close 调用为您完成。 @salva - 感谢您的澄清;不幸的是,负 PID 方法也不起作用。但是当我在父 Perl 进程上运行负信号时,它确实起作用了。所以看起来子流程与它在同一个流程组中? 【参考方案1】:

如果您只想读取 gzip 压缩文件的最后一行,使用纯 perl 即可轻松完成,无需调用外部程序:

#!/usr/bin/env perl
use warnings;
use strict;
use feature qw/say/;
use IO::Uncompress::Gunzip qw/$GunzipError/;

my $fname = 'foo.txt.gz';
my $z = new IO::Uncompress::Gunzip $fname or die "Couldn't open file: $GunzipError\n";
my $row;
while (<$z>) 
  $row = $_;

say "Row: $row";

【讨论】:

感谢肖恩的回复!不幸的是,我确实需要对这些文件做更多的事情,而不仅仅是阅读第一行或最后一行 - 我只是将其用作我注意到管道进程问题的最简单示例。但是我的应用程序作为一个整体正在对一些非常大的文件进行一些繁重的处理,因此出于性能原因,我决定使用 gzip 和 tac 而不是 Perl 模块来做同样的事情,因为它们似乎要快得多工作量类型(至少基于我开始项目时收集的信息)。 @dwillis77 考虑到当tac 的标准输入是管道而不是文件时,它必须先将所有内容读入内存,然后才能开始以相反的顺序打印行,在输出中使用它枪压缩一个大文件似乎不太可能比完全在 perl 中进行处理更好。 有趣的一点...我不介意在您的示例中给 Perl 模块一个镜头来比较解压缩性能。在与 tac 进行比较时,我主要研究了 ReadBackwards ——我没有将您提到的模块与 g(un)zip 进行比较。 但是,我不确定它是否必须将 100% 的文件读入内存才能开始从末尾打印行,因为我只是对 gzip 文件进行了向后查询(9GB 未压缩,仅 305MB(!)压缩),在一个总共只有 4GB RAM 的盒子上,它在一分钟多后就回来了(结果为 1K,这是我设置的上限),内存使用量仅徘徊在 1.4GB 左右在搜索期间并且没有交换使用(CPU 在搜索期间明显增加,但不是内存)。 Re "所以 File::ReadBackwards Perl 模块确实将整个内容加载到内存中,然后为了向后读取它?",不,它从末尾读取与从头开始读取文件的方式完全相同:读取一个块,在块中查找行,并在需要时读取其他块。 (因此,它需要一个可搜索的文件。)【参考方案2】:

发生这种情况是因为您的 perl 脚本或其父级忽略了 SIGPIPE 信号,而忽略信号的处置由子级继承。

这是一个更简单的测试用例:

$ perl -e '$SIGPIPE="IGNORE"; open my $fh, "-|", "seq 100000 | tac; true"; print scalar <$fh>'
100000
tac: write error
$ (trap "" PIPE; perl -e 'open my $fh, "-|", "seq 100000 | tac"; print scalar <$fh>')
100000
tac: write error
$ (trap "" PIPE; perl -e 'my $pid = open my $fh, "-|", "seq 100000 | tac"; print scalar <$fh>; kill 15, $pid; waitpid $pid, 0')
100000
$ tac: write error

后一个版本与来自 OP 的版本执行相同的kill,它不会杀死管道的右侧或左侧,但 shell 正在运行并等待两者(一些 shell 将通过左侧执行管道;使用这样的 shell,可以将 ; exit $? 附加到命令中以重现示例)。

在输入perl 脚本时忽略SIGPIPE 的情况是通过fastcgi 运行时——这会将SIGPIPE 处置设置为ignore,并将expects 脚本设置为处理它。在这种情况下,只需设置 SIGPIPE 处理程序而不是 IGNORE(甚至是空处理程序)即可,因为在这种情况下,信号处理将在执行外部命令时重置为默认值:

$SIGPIPE = sub  ;
open my $fh, '-|', 'trap - PIPE; ... | tac';

当作为独立脚本运行时,它可能是一些设置错误(我已经看到它发生在与 Linux 上的容器化相关的问题中),或者有人试图利用以提升的权限运行的错误程序,而不是处理 write(2) 错误(在这种情况下为EPIPE)。

my $pid = open3(my $in, my $fh, undef, "gunzip -c \"$fname\"");

...然后像我上面那样杀死它,我没有收到任何错误。

如果您将其 stderr 重定向到相同的 $fh 您只读取第一行,您应该从哪里得到错误?

open3 完全没有区别:

$ (trap "" PIPE; perl -MIPC::Open3 -e 'my $pid = open3 my $in, my $out, my $err, "seq 100000 | tac 2>/dev/tty"; print scalar <$out>')
100000
$ tac: write error

【讨论】:

嗯,我一定误解了 open3() 文档。如示例 here(以及 IPC::Open3 文档)中提到的,如果 *ERROR 为 false,它使用与 stdout 相同的文件句柄。在 Perl 中,undef 是 eval 为 false 的值之一,对吗?编辑/更新:好的,我明白了,是的,从标准输出中读取一行我们不会看到错误,如果它在那里。我错过了那个。 是的,我已经更正了这一点,尽管它并没有改变任何想法,真的。您必须阅读来自$fh 的错误消息。 感谢您的回复,它开始变得更清晰了。我能够确认 open() 和 open3() 生成过程的方式存在一些差异(请参阅我上面的评论或 convo 移至的聊天)。我根本没有弄乱SIGPIPE 处理(至少据我所知)。但这是从网页运行的 CGI 脚本。你说 Perl 的父母会影响这一点。是否有可能是 apache 导致进程忽略收到的SIGPIPE 因为如果我没记错的话(我会重新调整我的脚本来确认),我相信当我刚刚关闭文件句柄并且什么都不做(如此处所建议的)时,我在日志中得到了同样的错误。因此,我首先进行了这次狩猎。 当然,不用担心。这对命令有意义 - 未调用 sh -c 的 open3() 命令没有管道或特殊字符(仅限 gunzip,没有 tac),而带有 open() 的相同命令将有管道 (-|)。这里提供了很多有用的信息,你的回答确实解释了发生了什么(尽管我仍然想完全了解为什么这个过程忽略了SIGPIPE),所以我'我接受这个作为答案。感谢您花时间解释这些东西(我对 Perl 还是很陌生,你可能会说 - 这是我在其中的第一个大型项目)。

以上是关于Perl 行为差异关闭由 open() 产生的子进程与 IPC::Open3的主要内容,如果未能解决你的问题,请参考以下文章

window.open 打开子窗体,关闭全部的子窗体

多态的好处??

VBO 与立即模式 (glBegin/glEnd) 的行为是不是存在任何行为差异? [关闭]

为啥 Perl 的两个 arg open 似乎去掉了换行符?

我们在 C 或 C++ 中是不是有类似于 IPC::Open3 的 perl

每日学习:Perl语言学习之文件读写(open)