Reaping child processes from Perl
Asked Answered
W

4

11

I have a script that spawns a set of children. The parent must wait for each of the children to finish.

My script performs similar to the following perl script:

#! /usr/bin/perl
use strict;
use warnings;

print "I am the only process.\n";

my @children_pids;

for my $count (1..10){
        my $child_pid = fork();
        if ($child_pid) {  # If I have a child PID, then I must be the parent
                push @children_pids, $child_pid;
        }
        else { # I am the child
                my $wait_time = int(rand(30));
                sleep $wait_time;
                my $localtime = localtime;
                print "Child: Some child exited at $localtime\n";
                exit 0; # Exit the child
        }
}

foreach my $child (@children_pids) {
        print "Parent: Waiting on $child\n";
        waitpid($child, 0); 
        my $localtime = localtime;
        print "Parent: Child $child was reaped - $localtime.\n";
}

print "All done.\n";

Similar to the code I've provided above, each child may take a different time to finish.

The problem is when I try to reap the children by looping over the children PIDs, in that last foreach block, the parent waits for the children in the order that they are created.

Obviously the children do not finish in the order which they are spawned and so I'm left with a bunch of zombie processes for children that happen to finish early.

In my actual code, these children may finish days before one another and the number of zombie processes floating around can grow in the hundreds.

Is there a better way for me to reap a set of children?

Wistrup answered 6/6, 2012 at 23:5 Comment(0)
U
14

If your parent process doesn't need to be aware of its children's completion status then you can just set

$SIG{CHLD} = 'IGNORE';

which will automatically reap all children as they complete.

If you do need to be informed of the children completing, then the signal handler needs to be set to reap all possible processes

use POSIX ();

$SIG{CHLD} = sub {
  while () {
    my $child = waitpid -1, POSIX::WNOHANG;
    last if $child <= 0;
    my $localtime = localtime;
    print "Parent: Child $child was reaped - $localtime.\n";
  }
};
Underbody answered 6/6, 2012 at 23:18 Comment(8)
Primary reason to be informed: To see if they were successful.Zendejas
Hey Borodin, check out Syntax::Feature::LoopZendejas
@ikegami: yes I sometimes use that. I mostly flit between using while (1), while () and { ... redo; }. None are really satisfactory.Underbody
Why is your second argument to waitpid, the FLAGS argument, set to 1? What does that correspond to?Wistrup
@emiller: I've just edited it. It's WNOHANG on my machine but I realise that's not necessarily portable.Underbody
@Borodin, I use for (;;) and pronounce it "for ever"Zendejas
The C for loop has always seemed an ugly thing to me. It is incomprehensible without a prior knowledge of C and rarely what you really want to do. It has one advantage: that the continue clause is at the top of the loop where it belongs.Underbody
Why would one use the non-blocking version of waitpid? I see it both here and in the perlipc docs, but no description of why. Wouldn't (since I just received the death of a child) waitpid be guaranteed to return something in this instance? And that while() loop look like a quite unnecessary CPU spike to me... Am I missing something?Ponderable
B
6

use "-1" for the pid, or use the wait() function so that you wait for any child process. The reaped pid is returned, so you can check it against your list if necessary. If that is unacceptable, then periodically waitpid for each pid in your list with POSIX::WNOHANG() as the second argument.

Ballista answered 6/6, 2012 at 23:16 Comment(0)
N
5

Borodin's answer is perfectly fine for the asynchronous reaping of children as they terminate.

If, as your question and code suggest to me, you are looking for the synchronous (blocking) reaping of all outstanding children in the order in which they terminate, the parent can simply do this:

use feature qw(say);

...

# Block until all children are finished
while (1) {
  my $child = waitpid(-1, 0);
  last if $child == -1;       # No more outstanding children

  say "Parent: Child $child was reaped - ", scalar localtime, ".";
}

say "All done."
Natalienatalina answered 7/6, 2012 at 4:52 Comment(0)
D
1

Never use a loop like this to wait for children:

while (1) {
    my $child = waitpid(-1, POSIX::WNOHANG);
    last if $child == -1;
    print "Parent: Child $child was reaped\n";
}

The parent process will consume 100% cpu while waiting for the child processes to die - especially when they can run for a long time. At least add a sleep (bad idea - when they die fast, the parent is waiting).

Always use a blocking wait + count for TERM/INT/ppid for niceness!:

my $loop = 1;
$SIG{CHLD} = 'DEFAULT';  # turn off auto reaper
$SIG{INT} = $SIG{TERM} = sub {$loop = 0; kill -15 => @children_pids};
while ($loop && getppid() != 1) {
    my $child = waitpid(-1, 0);
    last if $child == -1;
    print "Parent: Child $child was reaped\n";
}

This blocking wait it of course not possible when the parent process also has to do other stuff - like the getppid() call ;-). For that, you can use a socketpair() and put that in a select() that does a blocking call. Even the loop check can benefit from that.

Disenfranchise answered 9/6, 2012 at 9:52 Comment(2)
It shouldn't loop forever, that's what the 'last if' line does. If no child has died, it exits the loop.Ayah
It's not about the forever looping, it's about the 100% cpu cycles used during the looping. If it takes 10 minutes for 200 processes to end, it's 1 process wasting 100% cpu cycles for 10 minutes, while if you block, this is basically none.Disenfranchise

© 2022 - 2024 — McMap. All rights reserved.