Всегда ли безопасно комбинировать select (2) и буферизованный ввод-вывод для файловых дескрипторов? - PullRequest
5 голосов
/ 08 сентября 2011

Я использую IO :: Select для отслеживания переменного количества файловых дескрипторов для чтения. Документация, с которой я столкнулся, настоятельно рекомендует не объединять оператор select с <> (readline) для чтения из файловых дескрипторов.

Моя ситуация:

Я буду использовать каждый дескриптор файла только один раз, т. Е. Когда селектор предложит мне дескриптор файла, он будет полностью использован, а затем удален из селектора. Я буду получать хэш и переменное количество файлов. Я не против, если это блокирует на время.

Для большего контекста я являюсь клиентом, отправляющим информацию для обработки моими серверами. Каждый дескриптор файла - это отдельный сервер, с которым я разговариваю. Как только сервер будет завершен, мне будет возвращен результат хэширования от каждого. Внутри этого хеша находится число, указывающее количество файлов для подражания.

Я хочу использовать 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 ]

10 голосов
/ 08 сентября 2011

Использование readline (он же <>) совершенно неверно по двум причинам: оно буферизовано и блокируется.


Буферизация плохая

Точнее, буферизация с использованиембуферы, которые не могут быть проверены, являются плохими.

Система может выполнять всю необходимую буферизацию, поскольку вы можете просматривать ее буферы, используя select.

Системе ввода-вывода Perl нельзя разрешать делатьлюбая буферизация, потому что вы не можете заглянуть в ее буферы.

Давайте рассмотрим пример того, что может произойти с использованием 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
...

Это можно исправить с помощью небуферизованного ввода-вывода:

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, которая не блокирует и использует только bвы можете проверить.Вторая часть проста, потому что вы можете проверить созданные вами буферы.

  • Создать буфер для каждого дескриптора.
  • Когда данные поступают из дескриптора, прочитайте их, но не более.Когда данные ожидают (как мы знаем из select), sysread вернет то, что доступно, не дожидаясь поступления большего количества.Это делает sysread идеальным для этой задачи.
  • Добавляет считанные данные в соответствующий буфер.
  • Для каждого полного сообщения в буфере извлекает его и обрабатывает его.

Добавление дескриптора:

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

select loop:

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 может выполнить всю тяжелую работу за вас, и что асинхронный ввод-вывод может использоваться в качестве альтернативы select.

1 голос
/ 09 сентября 2011

После долгих обсуждений с @ikegami мы определили, что в моем крайне специфическом случае readline на самом деле не проблема.Я все еще оставляю ikegami в качестве принятого правильного ответа, потому что это, безусловно, лучший способ справиться с общей ситуацией и замечательная рецензия.

Readline (aka <>) приемлем в моей ситуации из-заследующие факты:

  • Дескриптор возвращается только один раз из оператора select, а затем он закрывается / удаляется
  • Я отправляю только одно сообщение через дескриптор файла
  • Мне все равно, обрабатывает ли блок дескрипторов
  • Я учитываю тайм-ауты и возврат закрытых дескрипторов из select (проверка ошибок не включена в приведенный выше пример кода)
...