在执行其他操作时非阻塞读取管道

时间:2018-02-01 00:24:21

标签: perl pipe

作为process hangs when writing large data to pipe的后续,我需要为父进程实现一种方法,以便从子进程读取的管道中读取,同时执行其他操作直到子进程完成。

更具体地说,父节点通过HTTP返回对客户端的响应。响应由字符串<PING/>组成,在完成ping操作后跟随字符串<DONE/>,然后是实际内容。这样做是为了使连接保持活动状态,直到实际响应准备就绪。

我的问题:

1)我主要只是寻找一般反馈。你看到这个代码有什么问题吗?

2)我是否会实现无阻塞读取的目标?特别是一旦读取了所有当前可用的数据(但作者仍在写更多内容),我的代码是否能够从while ( my $line = <$pipe_reader>){继续?在管道关闭之后但在孩子终止之前它会正常工作吗?

3)IO::Select的文档说明add()需要IO::Handle个对象。我到处都看到IO::Handle,但我不知道如何确定以这种方式创建的管道是否算作IO::Handle个对象。 perl -e "pipe(my $r, my $w); print(ref($r))"只是给了我GLOB ......

4)select的Perl文档(我假设IO::Select所基于的)警告

  

警告:除非POSIX允许,否则不应尝试将缓冲的I / O(如readreadline)与select混合使用,即使只是在POSIX系统上也是如此。您必须改为使用sysread

这是否意味着在同一个循环中拥有$writer->write('<PING/>');是一个问题?

Perl代码

pipe(my $pipe_reader, my $pipe_writer);
$pipe_writer->autoflush(1);

my $pid = fork;

if ( $pid ) {

    # parent
    close $pipe_writer;

    $s = IO::Select->new();
    $s->add($pipe_reader);

    my $response  = "";
    my $startTime = time;
    my $interval  = 25;
    my $pings     = 0;

    while ( waitpid(-1, WNOHANG) <= 0 ) {

        if ( time > $startTime + ($interval * $pings) ) {
            $pings++;
            $writer->write('<PING/>');
        }

        if ( $s->can_read(0) ) {

            while ( my $line = <$pipe_reader> ) {
                $response .= $line;
            }
        }
    };

    $writer->write('<DONE/>');
    $writer->write($response);
    close $pipe_reader;
    $writer->close();

else {

    #child
    die "cannot fork: $!" unless defined $pid;
    close $pipe_reader;

    #...do writes here...

    close $pipe_writer;
}

关于$writer,它可能与此问题无关,但整体解决方案遵循的是模式 second code sample here

由于我们还没有准备好整个HTTP主体,我们返回一个回调给PSGI,它给了我们一个$responder对象。我们只提供HTTP状态和内容类型,然后它为我们提供了$writer以便稍后编写正文。

我们在上面的代码中使用$writer来编写ping值和最终的body。所有上面的代码都在回调给PSGI的回调中,但为了简洁起见,我省略了它。

1 个答案:

答案 0 :(得分:2)

这里的第一个问题是非阻塞操作。其他问题将在下面解决。

正如您所引用的那样,使用select(或IO::Select)时,不应使用缓冲I / O.特别是在这里你想要非阻塞和非缓冲操作。下面的代码与<>非常混淆。

请注意&#34;缓冲&#34;是一个多层次的业务。其中一些可以通过一个简单的程序指令打开/关闭,一些更难以搞砸,一些是实现的问题。它在语言,库,操作系统,硬件中。我们至少可以使用推荐的工具。

因此,使用sysreadselect - 操纵句柄中读取,而不是readline<>使用的内容)。它会在0上返回EOF,因此可以测试写入结束何时结束(发送EOF时)。

use warnings;
use strict;
use feature 'say';

use Time::HiRes qw(sleep);
use IO::Select; 

my $sel = IO::Select->new;

pipe my $rd, my $wr;
$sel->add($rd); 

my $pid = fork // die "Can't fork: $!";  #/

if ($pid == 0) {
    close $rd; 
    $wr->autoflush;
    for (1..4) {
        sleep 1;
        say "\tsending data";
        say $wr 'a' x (120*1024);
    }
    say "\tClosing writer and exiting";
    close $wr;
    exit; 
}   
close $wr;    
say "Forked and will read from $pid";

my @recd;
READ: while (1) {
    if (my @ready = $sel->can_read(0)) {  # beware of signal handlers
        foreach my $handle (@ready) {
            my $buff;
            my $rv = sysread $handle, $buff, 64*1024;
            if (not $rv) {  # error (undef) or closed writer (==0)
                if (not defined $rv) {
                    warn "Error reading: $!";
                }
                last READ;  # single pipe (see text)
            }
            say "Got ", length $buff, " characters";
            push @recd, length $buff; 
        }
    }
    else {
        say "Doing else ... ";
        sleep 0.5; 
    }
}   
close $rd;
my $gone = waitpid $pid, 0;
say "Reaped pid $gone";
say "Have data: @recd"

这假设父母在else中没有进行大量处理,或者那会使管道检查等待。在这种情况下,您需要为那些长期工作分配另一个流程。

一些评论

  • 我要求来自sysread的大量数据,因为这是使用它的最有效方式,并且正如您期望来自孩子的大写。您可以从打印件(下面的示例)中看到它是如何工作的。

  • sysread的未定义返回表示错误。管道可能仍然可读,如果我们通过sysread返回while,我们可能会遇到无限循环的错误,因此我们退出循环。读错误可能不会在下次发生,但依靠它会冒无限循环的风险。

  • 在异常返回(编写器关闭或错误)时,代码退出循环,因为此处不再需要。但是对于更复杂的IPC(更多管道,所有这些在另一个循环中采用新连接,信号处理程序等),我们需要从要监视的列表中删除句柄,并且读取错误的处理将与封闭作家的作品。

  • 在这个简单的例子中,错误处理很简单(实际上只是last READ if not $rv;)。但一般来说,读取错误与有序封闭式写入器不同,它们是分开处理的。 (例如,在读取错误时,我们想要重试固定的次数。)

  • 使用$buffOFFSET的第四个参数sysread,可以将所有数据收集到length $buff。然后每次写入都从$buff的末尾开始,这将被扩展。

    my $rv = sysread $handle, $buff, 64*1024, length $buff;
    

    在这种情况下,不需要@recd。这是收集数据的常用方法。

  • 信号是任何IPC的重要组成部分。

  • 进行了有限的讨论

"Safe signals"通常可以防止I / O被信号中断。但select可能会受到影响

  

请注意,在信号(例如,SIGALRM)与实现相关之后是否重新启动select

因此使用它的句柄也可能不安全。根据我的经验,can_read可以在程序处理SIGCHLD时返回(false)。这个简单的例子是安全的,原因如下:

  • 如果can_read在处理信号时返回空,则while会将其恢复到该句柄,该句柄仍然可读。

  • 如果程序在select被阻止,则信号会影响select。但是你有非阻塞操作,并且select正在检查句柄时信号进入的可能性微乎其微

  • 最后,我不知道写入管道的进程的SIGCHLD是否会影响该管道另一端的select,但即使它赔率是天文数字小。

使用更复杂的代码(如果can_read不是直接在上面的循环中),请考虑其错误返回(由于信号)是否会影响程序流。如果这是一个问题,请添加代码以检查来自can_read的错误回报;如果信号导致$!EINTR。这可以使用%!来检查,Errno在使用时会加载signal handler。因此,您可以检查can_read是否因if $!{EINTR}的中断而返回。例如

if (my @ready = $sel->can_read(0)) {
   ...
}
elsif ($!{EINTR}) { 
   # interrupted by signal, transfer control as suitable
   next READ;
}

同样,上面的程序无论如何都会立即返回while(假设else块不适用于长时间运行的作业,应该有另一个进程)。< / p>

另一个问题是SIGPIPE信号,默认情况下会杀死该程序。由于您正在处理管道,因此只能谨慎处理它 安装follow-up post

    $SIG{PIPE} = \&handle_sigpipe;

其中sub handle_sigpipe可以执行程序所需的操作。例如,设置用于检查管道有效性的全局变量,因此一旦出现错误,我们就不会再次尝试读取/写入。我们已分配给$SIG{PIPE}这一事实可以保护该信号。但是,除非它'IGNORE',否则需要重新启动can_read,如上所述。请参阅this post

对问题的评论

  • 您的代码片段无法 继续&#34;因为它使用<>来阅读。 (此外,你进入while超过<>,它确实阻止。所以一旦它读取可用的东西,它就会坐下来等待更多来。你想要一次读取,但不是与<>。)

  • 每个文件框架都是IO::Handle(或IO::File)对象,或者至少可以根据需要加入这些类。请参阅socketpair的第二部分。

  • 不将缓冲I / O与select混合的警告与使用它的文件句柄有关。虽然它对管道至关重要,但写入其他服务是无关的。

  • 代码评论:没有必要对儿童退出的所有工作进行调整。您需要注意孩子关闭管道的时间。稍后收集过程(收集信号)。

处理类似需求的另一种方法是在自己的fork中完成工作的每个部分。所以要做到“保持活力”。将您的HTTP放在一个单独的过程中。然后,通过使用this post进行通信,父母可以更简单地管理所有子流程。

请参阅{{3}},了解包含许多相关要点的readsysread的比较。

上面的代码打印

Forked and will read from 4171
Doing else ... 
Doing else ... 
Doing else ... 
        sending data
Got 65536 characters
Got 57345 characters
Doing else ... 
Doing else ... 
        sending data
Got 65536 characters
Got 57345 characters
Doing else ... 
Doing else ... 
        sending data
Doing else ... 
Got 65536 characters
Got 40960 characters
Got 16385 characters
Doing else ... 
Doing else ... 
        sending data
Got 65536 characters
Got 24576 characters
        Closing writer and exiting
Got 32769 characters
Doing else ... 
Reaped pid 4171
Have data: 65536 57345 65536 57345 65536 40960 16385 65536 24576 32769