【问题标题】:Is it ever safe to combine select(2) and buffered IO for file handles?将 select(2) 和缓冲 IO 结合起来用于文件句柄是否安全?
【发布时间】:2011-09-08 13:50:20
【问题描述】:

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

我的情况:

I will only ever use each file handle once, i.e. when the select offers me the file handle, it will be completely used and then removed from the 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
    }
}

【问题讨论】:

  • 你有一些示例代码来展示你做了什么吗?
  • 添加了我正在尝试做的示例。
  • { my $oldfh = select $rh; $| = 1; select $oldfh; } 对读取句柄毫无用处。这是一件好事,因为如果 Perl 在每次读取后都按照您的意愿清空缓冲区,您就会丢失数据!

标签: perl io buffer


【解决方案1】:

使用readline(又名&lt;&gt;)是完全错误的,原因有两个:它是缓冲的,它是阻塞的。


缓冲不好

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

系统可以进行它想要的所有缓冲,因为您可以使用select 查看它的缓冲区。

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

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

  • "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
...

阻塞不好

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

  • "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 的替代方案。

【讨论】:

  • 我确切知道从文件句柄中读取多少内容是否重要? select 给了我?我读取了我的哈希值,然后读取了由哈希值确定的多个文件。
  • @Lomsky,而不是在使用 sysread 时。 sysread 将返回可用的内容,仅此而已。我会把它添加到帖子中。
  • 我的意思是,因为我确切地知道文件句柄上返回的内容(并且所有内容都立即返回),所以缓冲区真的很重要吗?
  • @Lomsky,您将所有内容一次性退回的假设是不正确的。你无法控制它。你知道什么是发送,而不是什么接收。如果您知道收到了什么,就没有理由使用select
  • @Lomsky,添加了说明,解释了为什么当我声称“缓冲不好”时使用自己的缓冲区是安全的。
【解决方案2】:

在与@ikegami 进行了多次讨论后,我们确定在我极其具体的情况下,readline 实际上不是问题。我仍然将 ikegami 的答案作为公认的正确答案,因为它无疑是处理一般情况的最佳方法,而且是一篇精彩的文章。

Readline(又名 )在我的情况下是可以接受的,原因如下:

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

【讨论】:

    猜你喜欢
    • 2012-10-15
    • 1970-01-01
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    • 1970-01-01
    • 2013-10-19
    • 2017-10-17
    • 2014-04-05
    相关资源
    最近更新 更多