将select(2)和缓冲IO组合成文件句柄是否安全?

时间:2011-09-08 13:50:21

标签: perl io buffer

我正在使用IO :: Select来跟踪可变数量的文件句柄以供阅读。我遇到的文档强烈建议不要将select语句与<>结合使用。 (readline)用于从文件句柄中读取。

我的情况:

我只会使用一次文件句柄,即当select为我提供文件句柄时,它将被完全使用,然后从select中删除。我将收到一个哈希值和一个可变数量的文件。我不介意这会阻塞一段时间。

有关更多上下文,我是发送信息以供我的服务器处理的客户端。每个文件句柄都是我正在与之交谈的不同服务器。服务器完成后,哈希结果将从每个服务器发回给我。在该哈希内部是一个数字,表示要遵循的文件数。

我希望使用readline以便与现有项目代码集成以传输Perl对象和文件。

示例代码:

my $read_set = IO::Select()->new;
my $count = @agents_to_run; #array comes as an argument

for $agent ( @agents_to_run ) { 
    ( $sock, my $peerhost, my $peerport ) 
        = server($config_settings{ $agent }->
            { 'Host' },$config_settings{ $agent }->{ 'Port' };
    $read_set->add( $sock );

}

while ( $count > 0) {
    my @rh_set = IO::Select->can_read();

    for my $rh ( @{ $rh_set } ) {

            my %results = <$rh>;
            my $num_files = $results{'numFiles'};
            my @files = ();
            for (my i; i < $num_files; i++) {
                    $files[i]=<$rh>;
            }                 
            #process results, close fh, decrement count, etc
    }
}

2 个答案:

答案 0 :(得分:10)

使用readline(又名<>)是完全错误的,原因有二:它是缓冲的,而且是阻塞的。


缓冲不好

更确切地说,使用无法检查的缓冲区进行缓冲是不好的。

系统可以执行所需的所有缓冲,因为您可以使用select查看其缓冲区。

Perl的IO系统不允许进行任何缓冲,因为你无法查看其缓冲区。

让我们看一个使用readline循环中的select会发生什么的示例。

  • "abc\ndef\n"到达手柄。
  • select通知您有数据需要阅读。
  • readline将尝试从句柄中读取一个块。
  • "abc\ndef\n"将被放置在Perl的句柄缓冲区中。
  • readline将返回"abc\n"

此时,您再次致电select,并希望它让您知道还有更多内容需要阅读("def\n")。但是,select将报告无法读取,因为select是系统调用,并且数据已从系统中读取。这意味着您必须等待更多内容才能阅读"def\n"

以下程序说明了这一点:

use IO::Select qw( );
use IO::Handle qw( );

sub producer {
    my ($fh) = @_;
    for (;;) {
        print($fh time(), "\n") or die;
        print($fh time(), "\n") or die;
        sleep(3);
    }
}

sub consumer {
    my ($fh) = @_;
    my $sel = IO::Select->new($fh);
    while ($sel->can_read()) {
        my $got = <$fh>;
        last if !defined($got);
        chomp $got;
        print("It took ", (time()-$got), " seconds to get the msg\n");
    }
}

pipe(my $rfh, my $wfh) or die;
$wfh->autoflush(1);
fork() ? producer($wfh) : consumer($rfh);

输出:

It took 0 seconds to get the msg
It took 3 seconds to get the msg
It took 0 seconds to get the msg
It took 3 seconds to get the msg
It took 0 seconds to get the msg
...

这可以使用非缓冲IO修复:

sub consumer {
    my ($fh) = @_;
    my $sel = IO::Select->new($fh);
    my $buf = '';
    while ($sel->can_read()) {
        sysread($fh, $buf, 64*1024, length($buf)) or last;
        while ( my ($got) = $buf =~ s/^(.*)\n// ) {
            print("It took ", (time()-$got), " seconds to get the msg\n");
        }
    }
}

输出:

It took 0 seconds to get the msg
It took 0 seconds to get the msg
It took 0 seconds to get the msg
It took 0 seconds to get the msg
It took 0 seconds to get the msg
It took 0 seconds to get the msg
...

阻止不好

让我们看一个使用readline循环中的select会发生什么的示例。

  • "abc\ndef\n"到达手柄。
  • select通知您有数据需要阅读。
  • readline将尝试从套接字读取一个块。
  • "abc\ndef\n"将被放置在Perl的句柄缓冲区中。
  • readline尚未收到换行符,因此它会尝试从套接字中读取另一个块。
  • 目前没有更多数据可用,因此会阻止。

这违背了使用select的目的。

[演示代码即将发布]


解决方案

您必须实现不阻止的readline版本,并且只使用您可以检查的缓冲区。第二部分很简单,因为您可以检查您创建的缓冲区。

  • 为每个句柄创建一个缓冲区。
  • 当数据从句柄到达时,请阅读但不能再阅读。当数据等待时(正如我们从select所知),sysread将返回可用的内容,而无需等待更多内容到达。这使sysread完美地完成了这项任务。
  • 将读取的数据附加到适当的缓冲区。
  • 对于缓冲区中的每条完整消息,将其解压缩并处理。

添加句柄:

$select->add($fh);
$clients{fileno($fh)} = {
    buf  => '',
    ...
};

select循环:

while (my @ready = $select->can_read) {
    for my $fh (@ready) {
        my $client = $clients{fileno($fh)};
        our $buf; local *buf = \($client->{buf});  # alias $buf = $client->{buf};

        my $rv = sysread($fh, $buf, 64*1024, length($buf));
        if (!$rv) {
            if (!defined($rv)) {
                ... # Handle error
            }
            elsif (length($buf)) {
                ... # Handle eof with partial message
            }
            else {
                ... # Handle eof
            }

            delete $clients{fileno($fh)};
            $sel->remove($fh);
            next;
        }

        while ( my ($msg) = $buf =~ s/^(.*)\n// )
            ... # Process message.
        }
    }
}

顺便说一下,使用线程更容易,这甚至不能处理编写器!


请注意,IPC::Run可以为您完成所有艰苦的工作,并且异步IO可以用作select的替代方案。

答案 1 :(得分:1)

经过与@ikegami的多次讨论,我们确定在我极其具体的情况下,readline实际上不是问题。我仍然离开ikegami作为公认的正确答案,因为它是处理一般情况的最佳方式,也是一篇精彩的文章。

由于以下事实,

Readline(又名&lt;&gt;)在我的情况下是可以接受的:

  • 句柄仅从select语句返回一次,然后关闭/删除
  • 我只通过文件句柄发送一条消息
  • 我不在乎读句柄阻止
  • 我考虑了select的超时和关闭句柄返回(错误检查不包含在上面的示例代码中)