Search code examples
linuxperlsshforkzombie-process

How to avoid <defunct> processes?


I do ZFS remote replication from a master host to a slave host, where I have a Perl script that runs on the master host.

For each filesystem it ssh to the remote host and start mbuffer in listening mode and then the script continues and then send the data. On success mbuffer should exit by itself.

The problem

It was quite difficult to start mbuffer on the remote host over ssh and then be able to continue in the script. I ended up doing what you can see below.

The problem is that until the script exits it leaves behind <defunct> processes one for each filesystem.

Question

It is possible to avoid having the <defunct> processes?

sub mbuffer {
    my ($id, $zfsPath) = @_;

    my $m = join(' ', $mbuffer, '-I', $::c{port});
    my $z = join(' ', $zfs, 'receive', , $zfsPath);
    my $c = shellQuote($ssh, $::c{slaves}{$id}, join('|', $m, $z));

    my $pm = Parallel::ForkManager->new(1);
    my $pid = $pm->start;
    if (!$pid) {
        no warnings;  # fixes "exec" not working
        exec($c);
        $pm->finish;
    }

    sleep 3; # wait for mbuffer to listen

    return $pid;
}

Solution

  • When you create a process, it sticks around until its parent reaps it. (If its parent exits first, it will get auto-reaped.) A process can reap its children using wait or waitpid. It can also cause its children to be automatically reaped by using local $SIG{CHLD} = 'IGNORE'; before creating the child.


    Note that Parallel::ForkManager is not the right tool for the job of launching a single child. It's not its purpose to spawn a single worker.

    use String::ShellQuote qw( shell_quote );
    
    sub mbuffer {
        my ($id, $zfsPath) = @_;
    
        my $mbuffer_cmd = shell_quote($mbuffer, '-I', $::c{port});
        my $zfs_cmd     = shell_quote($zfs, 'receive', $zfsPath);
        my $remote_cmd  = "$mbuffer_cmd | $zfs_cmd";
        my $local_cmd   = shell_quote($ssh, $::c{slaves}{$id}, $remote_cmd);
    
        # open3 will close this handle.
        # open3 doesn't deal well with lexical handles.
        open(local *CHILD_STDIN, '<', '/dev/null') or die $!;    
    
        return open3('<&CHILD_STDIN', '>&STDOUT', '>&STDERR', $local_cmd);
    }
    

    IPC::Open3 is quite low level, but it's the closest to your existing code. Better ways of launching processes include IPC::Run3 and IPC::Run.