What are the ways that signals can interfere with

2019-05-21 02:28发布

问题:

I don't know anything about signals, and only a little about pipes.

From the comments on zdim's answer here it seems that signals may interfere with pipe communication between parent and child processes.

I was told that, if you're using IO::Select and sysread, then the exit of a child process could somehow mess up the behavior of IO::Select::can_read, especially if there are multiple child processes.

Please describe how to account for signals when using pipes? The below code is an example where signals are not accounted for.

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 ) {     # Child code

    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; 
}

# Parent code
close $wr;    
say "Forked and will read from $pid";

my @recd;

READ:
while ( 1 ) {

    if ( my @ready = $sel->can_read(0) ) {  # beware of signals

        foreach my $handle (@ready) {

            my $buff;
            my $rv = sysread $handle, $buff, ( 64 * 1024 );
            warn "Error reading: $!" if not defined $rv;

            if ( defined $buff and $rv != 0 ) {
                say "Got ", length $buff, " characters";
                push @recd, length $buff; 
            }

            last READ if $rv == 0;
        }
    }
    else {
        say "Doing else ... ";
        sleep 0.5; 
    }
}   
close $rd;

my $gone = waitpid $pid, 0;

say "Reaped pid $gone";
say "Have data: @recd"

回答1:

Two things.

  1. Writing to a pipe after the reader was closed (e.g. perhaps because the process on the other end exited) leads to a SIGPIPE. You can ignore this signal ($SIG{PIPE} = 'IGNORE';) in order to have the write to return error EPIPE instead.

    In your case, if you wanted to handle that error instead of having your program killed, simply add

    $SIG{PIPE} = 'IGNORE';
    
  2. If you have any signal handler defined (e.g. using $SIG{...} = sub { ... };, but not $SIG{...} = 'IGNORE'; or $SIG{...} = 'DEFAULT';), long-running system calls (e.g. reading from a file handle) can be interrupted by a signal. If this happens, they will return with error EINTR to give the signal handler a chance to run. In Perl, you don't have to do anything but restart the system call that failed.

    In your case, you have no signal handlers defined, so this doesn't affect you.


By the way, you check $rv == 0 even when $rv is known to be undefined, and you place the length of the data in @recd instead of the data itself. In fact, it doesn't make much sense to use an array there at all. Replace

my @recd;

...

my $rv = sysread $handle, $buff, ( 64 * 1024 );
warn "Error reading: $!" if not defined $rv;

if ( defined $buff and $rv != 0 ) {
    say "Got ", length $buff, " characters";
    push @recd, length $buff; 
}

last READ if $rv == 0;

...

say "Have data: @recd"

with

my $buf = '';

...

my $received = sysread($handle, $buf, 64 * 1024, length($buf));
warn "Error reading: $!" if !defined($received);
last if !$received;

say "Got $received characters";

...

say "Have data: $buf"


回答2:

Signals may also interrupt I/O functions, causing then to fail with $! set to EINTR. So you should check for that error and retry when it happens.

Not doing it is a common source of hard to find bugs.



标签: perl pipe ipc