由于缓冲导致的死锁。它是如何运作的?

时间:2016-10-22 07:45:19

标签: perl deadlock

以下问题是对this答案中有关死锁的评论的回应。我很好奇死锁是如何产生的,所以我创建了一个测试程序:有一个父进程将大量数据写入子进程STDIN,而子进程也将大量数据写入父进程的读取器句柄。事实证明,如果数据大小超过80K(Ubuntu 16.04),将会出现死锁:

parent.pl

use feature qw(say);
use strict;
use warnings;

use IPC::Open2;

my $test_size = 80_000; # How many bytes to write?
my $pid = open2( my $reader, my $writer, 'child.pl' );

my $long_string = '0123456789' x ($test_size / 10);
printf "Parent: writing long string ( length: %d )\n", length $long_string;
print $writer $long_string;
close $writer;
say "Parent: Trying to read childs ouput..";
my $output = do { local $/; <$reader> };
printf "Parent: Got output with length %d..\n", length $output;
close $reader;
say "Parent: Reaping child..";
waitpid( $pid, 0 );
my $child_exit_status = $? >> 8;
say "Parent: Child exited with status: $child_exit_status";

child.pl

use feature qw(say);
use strict;
use warnings;

my $test_size = 80_000; # How many bytes to write?
my $child_log_filename = 'childlog.txt';

open ( my $log, '>', $child_log_filename ) or die "Could not create log file: $!";

say $log "Child is running..";

my $long_string = '0123456789' x ($test_size / 10);
say $log "Length of output string: " . length $long_string;
say $long_string;

my $input = do { local $/; <STDIN> };
say $log "Length of input string: " . length $input;
exit 2;

为什么这个程序会死锁?

1 个答案:

答案 0 :(得分:3)

死锁的原因似乎是管道在写入时被填满,没有人从中读取。根据{{​​3}}:

  

如果进程尝试写入完整管道(见下文),那么   写入(2)块直到从管道读取足够的数据   允许写完成。

     

管道容量有限。如果管道已满,则写入(2)   将阻塞或失败,具体取决于是否设置了O_NONBLOCK标志。   不同的实施方式对管道容量有不同的限制。   应用程序不应依赖于特定容量:应用程序   应该设计为使读取过程尽快消耗数据   它是可用的,因此写入过程不会被阻止。

     

在2.6.11之前的Linux版本中,管道的容量是相同的   作为系统页面大小(例如,i386上的4096字节)。自Linux   2.6.11,管道容量为65536字节。从Linux 2.6.35开始,默认管道容量为65536字节

因此,父级将一个长字符串写入子级的STDIN管道,管道被填满,这会导致父级阻塞。与此同时,孩子正在向父母读者管道写一个长串,这也会被填满并且孩子会阻塞。因此,孩子正在等待父母从其阅读器管道中读取,而父母正在等待孩子从其STDIN管道中读取。所谓的僵局。

我尝试使用selectfcntlsysreadsyswrite来解决此问题。由于syswrite将阻止我们尝试写入超过管道容量的内容,因此我使用fcntl来使编写器处理非阻塞。在这种情况下,syswrite将尽可能多地写入管道,然后立即返回它实际写入的字节数。

注意:只需要更改parent.pl(这也是需要的,因为我们不应该假设可以访问孩子的来源)。这是 修改后的parent.pl将阻止死锁:

use feature qw(say);
use strict;
use warnings;

use Errno qw( EAGAIN );
use Fcntl;
use IO::Select;
use IPC::Open2;

use constant READ_BUF_SIZE => 8192;
use constant WRITE_BUF_SIZE => 8192;

my $test_size = 80_000; # How many bytes to write?
my $pid = open2( my $reader, my $writer, 'child.pl' );

make_filehandle_non_blocking( $writer );

my $long_string = '0123456789' x ($test_size / 10);
printf "Parent: writing long string ( length: %d )\n", length $long_string;
my $sel_writers = IO::Select->new( $writer );
my $sel_readers = IO::Select->new( $reader );
my $read_offset = 0;
my $write_offset = 0;
my $child_output = '';
while (1) {
    last if $sel_readers->count() == 0 && $sel_writers->count() == 0;
    my @sel_result = IO::Select::select( $sel_readers, $sel_writers, undef );
    my @read_ready = @{ $sel_result[0] };
    my @write_ready = @{ $sel_result[1] };
    if ( @write_ready ) {
        my $bytes_written = syswrite $writer, $long_string, WRITE_BUF_SIZE, $write_offset;
        if ( !defined $bytes_written ) {
            die "syswrite failed: $!" if $! != EAGAIN;
            $bytes_written = 0;
        }
        $write_offset += $bytes_written;
        if ( $write_offset >= length $long_string ) {
            $sel_writers->remove( $writer );
            close $writer;
        }
    }
    if ( @read_ready ) {
        my $bytes_read = sysread $reader, $child_output, READ_BUF_SIZE, $read_offset;
        if ( !defined $bytes_read ) {
            die "sysread failed: $!" if $! != EAGAIN;
            $bytes_read = 0;
        }
        elsif ( $bytes_read == 0 ) {
            $sel_readers->remove( $reader );
            close $reader;
        }
        $read_offset += $bytes_read;
    }
}
printf "Parent: Got output with length %d..\n", length $child_output;
say "Parent: Reaping child..";
waitpid( $pid, 0 );
my $child_exit_status = $? >> 8;
say "Parent: Child exited with status: $child_exit_status";

sub make_filehandle_non_blocking {
    my ( $fh ) = @_;

    my $flags = fcntl $fh, F_GETFL, 0
      or die "Couldn't get flags for file handle : $!\n";
    fcntl $fh, F_SETFL, $flags | O_NONBLOCK
      or die "Couldn't set flags for file handle: $!\n";

}