4

I am trying to figure out the correct way to handle a simple case of parent-child interprocess communication (IPC). The child sends messages to the parent through the child's STDOUT handle. The parent does not send any messages to the child (except for SIGPIPE if it dies). In addition both child and parent need to handle a SIGINT signal from the user at the terminal. The main difficulty for the parent process is to correctly pick up the child's exit status when the child dies from SIGINT or SIGPIPE.

parent.pl:

#! /usr/bin/env perl

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

my $child_pid = open ( my $fh, '-|', 'child.pl' ) or die "Could not start child: $!";

$SIG{INT} = sub {
    $SIG{CHLD}="IGNORE";
    die "Caught SIGINT"
};

my $child_error;

$SIG{CHLD} = sub {
    $SIG{INT}="IGNORE";
    waitpid $child_pid, 0;
    $child_error = $?;
    die "Caught SIGCHLD: Child exited.."
};

eval {
    while (1) {
        msg( "sleeping(1).." );
        sleep 1;
        #internal_failure();
        msg( "waiting for child input.." );
        my $line = <$fh>;
        if ( defined $line ) {
            chomp $line;
            msg( "got line: '$line'" );
        }
        else {
            die "Could not read child pipe.";
        }
        msg( "sleeping(2).." );
        sleep 2;
    }
};

if ( $@ ) {
    chomp $@;
    msg( "interrupted: '$@'" );
}

my $close_ok = close $fh; # close() will implicitly call waitpid()
if ( !$close_ok ) {
    msg( "Closing child pipe failed: $!" );
    if ( !defined $child_error ) {
        waitpid $child_pid, 0;
    }
}
if ( !defined $child_error ) {
    $child_error = $?;
}
my $child_signal = $child_error & 0x7F;
if ( $child_signal ) {
    msg( "Child died from signal: $child_signal" );
}
else {
    msg( "Child exited with return value: " . ($child_error >> 8) );
}
exit;

sub msg { say "Parent: " . $_[0]  }

sub internal_failure {
    $SIG{CHLD}="IGNORE";
    $SIG{INT}="IGNORE";
    die "internal failure";
}

child.pl:

#! /usr/bin/env perl

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

$SIG{PIPE} = sub {
    $SIG{INT}="IGNORE";
    die "Caught SIGPIPE: Parent died.";
};

$SIG{INT} = sub {
    $SIG{PIPE}="IGNORE";
    die "Caught SIGINT\n";  # For some reason a newline is needed here !?
};

#local $SIG{INT} = "IGNORE";

STDOUT->autoflush(1); # make parent see my messages immediately
msg( "running.." );
eval {
    sleep 2;
    say "Hello"; # should trigger SIGPIPE signal if parent is dead
    sleep 1;
};
if ( $@ ) {
    chomp $@;
    msg( "interrupted: '$@'" );
    exit 2;
}

msg( "exits" );
exit 1;

Normal output from running parent.pl from command line would be:

Parent: sleeping(1)..
Child: running..
Parent: waiting for child input..
Parent: got line: 'Hello'
Parent: sleeping(2)..
Child: exits
Parent: interrupted: 'Caught SIGCHLD: Child exited.. at ./parent.pl line 20, <$fh> line 1.'
Parent: Closing child pipe failed: No child processes
Parent: Child exited with return value: 1

Question 1: Signal handlers

Is it correct to disable the other signals in a given signal handler? For example in the parent's SIGINT handler I have

$SIG{CHLD}="IGNORE";

to avoid also receiving the SIGCHLD at later point. For example, if I did not disable the child signal, it could arrive in the cleanup part (after the eval block) in the parent, and make the parent die before it has finished its cleanup.

Question 2: Handling SIGINT

If I press CTRL-C after starting the parent, the output typically looks like:

Parent: sleeping(1)..
Child: running..
Parent: waiting for child input..
^CChild: interrupted: 'Caught SIGINT'
Parent: interrupted: 'Caught SIGINT at ./parent.pl line 11.'
Parent: Closing child pipe failed: No child processes
Parent: Child died from signal: 127

The problem here is the exit status of the child. It should be 2, but instead it is killed by signal 127. What is the meaning of signal 127 here?

Question 3: Parent dies from internal failure

If I uncomment the line

#internal_failure();

in parent.pl, the output is:

Parent: sleeping(1)..
Child: running..
Parent: interrupted: 'internal failure at ./parent.pl line 71.'
Child: interrupted: 'Caught SIGPIPE: Parent died. at ./child.pl line 9.'
Parent: Closing child pipe failed: No child processes
Parent: Child died from signal: 127

This seems to work well except for the exit status from the child process. It should be 2, instead it is killed by signal 127.

Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
  • `$child_error` is probably `-1` from using `waitpid` outside of the signal handler. (`-1 & 0x7F == 0x7F == 127`). Closing the handle also calls `waitpid`. You have a mess. Pick where you want to call waitpid, and eliminate the other two. – ikegami Sep 19 '16 at 21:41
  • Speaking of messes, those `$SIG{xxx} = "IGNORE";` all over the place! wow. Why?!?! – ikegami Sep 19 '16 at 21:41
  • Note: Generally speaking, you need to have a loop in the SIGCHLD handler because multiple children might have died. – ikegami Sep 19 '16 at 21:41
  • @ikegami Thanks for the comment! Regarding the loop in SIGCHLD: I was considering that, but since I only consider a single child I thought it was not necessary – Håkon Hægland Sep 19 '16 at 21:42
  • Like I said, "generally speaking". Here, like you said, there's no problem. – ikegami Sep 19 '16 at 21:43
  • @ikegami I tried to expain the reason for why I disable the other handlers ( `$SIG{XXX}="IGNORE"` ) in Question 1. – Håkon Hægland Sep 19 '16 at 21:45

1 Answers1

3

You set the children to be automatically reaped ($SIG{CHLD} = "IGNORE";), then you called waitpid not once but twice more!

The subsequent calls to waitpid set $? to -1 (signaling an error, but that you misinterpret as "killed by signal"), and $! to No child processes.

Fixes:

$ diff -u ./parent.pl{~,}
--- ./parent.pl~        2016-09-19 19:28:39.778244653 -0700
+++ ./parent.pl 2016-09-19 19:28:10.698227008 -0700
@@ -7,16 +7,12 @@
 my $child_pid = open ( my $fh, '-|', 'child.pl' ) or die "Could not start child: $!";

 $SIG{INT} = sub {
-    $SIG{CHLD}="IGNORE";
+    $SIG{CHLD}="DEFAULT";
     die "Caught SIGINT"
 };

-my $child_error;
-
 $SIG{CHLD} = sub {
     $SIG{INT}="IGNORE";
-    waitpid $child_pid, 0;
-    $child_error = $?;
     die "Caught SIGCHLD: Child exited.."
 };

@@ -44,29 +40,19 @@
     msg( "interrupted: '$@'" );
 }

-my $close_ok = close $fh; # close() will implicitly call waitpid()
-if ( !$close_ok ) {
-    msg( "Closing child pipe failed: $!" );
-    if ( !defined $child_error ) {
-        waitpid $child_pid, 0;
-    }
-}
-if ( !defined $child_error ) {
-    $child_error = $?;
-}
-my $child_signal = $child_error & 0x7F;
-if ( $child_signal ) {
-    msg( "Child died from signal: $child_signal" );
-}
-else {
-    msg( "Child exited with return value: " . ($child_error >> 8) );
-}
+close $fh; # close() will implicitly call waitpid()
+
+if    ( $? == -1  ) { msg( "Closing child pipe failed: $!" ); }
+elsif ( $? & 0x7F ) { msg( "Child died from signal ".( $? & 0x7F ) ); }
+elsif ( $? >> 8   ) { msg( "Child exited with error ".( $? >> 8 ) ); }
+else                { msg( "Child executed successfully" ); }
+
 exit;

 sub msg { say "Parent: " . $_[0]  }

 sub internal_failure {
-    $SIG{CHLD}="IGNORE";
+    $SIG{CHLD}="DEFAULT";
     $SIG{INT}="IGNORE";
     die "internal failure";
 }

Fixed parent.pl:

#! /usr/bin/env perl

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

my $child_pid = open ( my $fh, '-|', 'child.pl' ) or die "Could not start child: $!";

$SIG{INT} = sub {
    $SIG{CHLD}="DEFAULT";
    die "Caught SIGINT"
};

$SIG{CHLD} = sub {
    $SIG{INT}="IGNORE";
    die "Caught SIGCHLD: Child exited.."
};

eval {
    while (1) {
        msg( "sleeping(1).." );
        sleep 1;
        #internal_failure();
        msg( "waiting for child input.." );
        my $line = <$fh>;
        if ( defined $line ) {
            chomp $line;
            msg( "got line: '$line'" );
        }
        else {
            die "Could not read child pipe.";
        }
        msg( "sleeping(2).." );
        sleep 2;
    }
};

if ( $@ ) {
    chomp $@;
    msg( "interrupted: '$@'" );
}

close $fh; # close() will implicitly call waitpid()

if    ( $? == -1  ) { msg( "Closing child pipe failed: $!" ); }
elsif ( $? & 0x7F ) { msg( "Child died from signal ".( $? & 0x7F ) ); }
elsif ( $? >> 8   ) { msg( "Child exited with error ".( $? >> 8 ) ); }
else                { msg( "Child executed successfully" ); }

exit;

sub msg { say "Parent: " . $_[0]  }

sub internal_failure {
    $SIG{CHLD}="DEFAULT";
    $SIG{INT}="IGNORE";
    die "internal failure";
}

The signal handling is still quite messy, but I wanted to avoid changing code unrelated to the fix.

Community
  • 1
  • 1
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • Thanks for the cleanup, this looks good! I see that removing `waitpid` from inside the child signal handler seems to work, but according to [perlvar](http://perldoc.perl.org/perlvar.html): *"If you have installed a signal handler for SIGCHLD , the value of $? will usually be wrong outside that handler."*. So how to interpret this statement from the documentation? – Håkon Hægland Sep 20 '16 at 07:08
  • 1
    That means $? might not be correct if you set it in the handler but check it outside the handler. But in this case, you call waitpid outside the handler, so of course you can check $? there. – ikegami Sep 20 '16 at 12:59
  • @zdim *"When I swap them it behaves much better."* I am not sure I understand. What do you mean by *swap* here? – Håkon Hægland Sep 20 '16 at 14:09
  • @ikegami *"The signal handling is still quite messy"*: Agree on that :) Any suggestions for improvements? The problem by removing the `$SIG{CHLD}="DEFAULT"` from the `SIGINT` handler (for example) is that the death of the child can arrive as a new signal when the parent is doing cleanup outside the eval loop. This will terminate the parent before it has finished the cleanup. Alternativly, the parent could add a new eval block around the cleanup code, but I think that would be even more messy.. – Håkon Hægland Sep 20 '16 at 15:26
  • 1
    `local` handlers within the `eval`? Set a flag instead of dying? – ikegami Sep 20 '16 at 16:39
  • 1
    @HåkonHægland I apologize for my uninformed comments on handlers involving other signals -- you nicely talk about it, what I failed to see. I do think that it needlessly over-complicates things but the comments were out of place since that is indeed intentional. I removed them. Sorry about that. – zdim Sep 22 '16 at 08:12
  • @zdim Thanks for the comment. No problem! I think now that maybe local handlers within the eval is the way to go here, as suggested by ikegami. Then the handler will automatically receive the values I have set in the scope outside the `eval` prior to entering the `eval`, or default values ( $SIG{INT}="DEFAULT", and so on ) if no such values have been set for the signal handlers outside the scope. For my case, I could use default values of `$SIG{INT}="IGNORE"` and `$SIG{CHLD}="IGNORE"` outside the eval. – Håkon Hægland Sep 22 '16 at 09:52
  • See also [What's the difference between various $SIG{CHLD} values?](http://stackoverflow.com/q/8389400/2173773) for more information. – Håkon Hægland Sep 22 '16 at 09:52