【问题标题】:How to avoid <defunct> processes?如何避免 <defunct> 进程?
【发布时间】:2018-01-17 09:58:54
【问题描述】:

我从主控主机到从属主机进行 ZFS 远程复制,我有一个在主控主机上运行的 Perl 脚本。

对于每个文件系统,它 ssh 到远程主机并以侦听模式启动 mbuffer,然后脚本继续,然后发送数据。成功时,mbuffer 应自行退出。

问题

在远程主机上通过 ssh 启动 mbuffer 并在脚本中继续运行非常困难。我最终做了你在下面看到的。

问题在于,在脚本退出之前,它会为每个文件系统留下一个 &lt;defunct&gt; 进程。

问题

可以避免&lt;defunct&gt; 进程吗?

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

【问题讨论】:

  • 父进程必须始终在其子进程上调用wait(或其变体之一),以便让内核知道可以清理终止的子进程。 This question 有一些答案可能会为您指明正确的方向。
  • 最快的解决方法是设置$SIG{CHLD}='IGNORE'。见perldoc -f fork
  • 失效进程或僵尸进程是在没有父进程调用wait 的情况下终止的进程。因此,内核保留终止子进程的条目,因此当父进程调用wait 时,它会返回所需的信息。为了避免僵尸,父进程需要等待它的子进程。

标签: linux perl ssh fork zombie-process


【解决方案1】:

当你创建一个进程时,它会一直存在,直到它的父进程获得它。 (如果它的父进程先退出,它将自动获取。)进程可以使用waitwaitpid 来获取它的子进程。它还可以通过在创建子节点之前使用local $SIG{CHLD} = 'IGNORE'; 导致其子节点被自动收割。


请注意,Parallel::ForkManager 不是启动单个子项的正确工具。产生一个工人不是它的目的。

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 的级别很低,但它最接近您现有的代码。启动进程的更好方法包括 IPC::Run3 和 IPC::Run。

【讨论】:

    【解决方案2】:

    一方面,没有理由在一个进程中使用P::FM。此外,这也是不利的,因为您放弃了对流程管理的更精细控制。

    但是这里的直接错误在于exec的使用;这篇文章只解决了这个问题。

    exec 调用将进程中的内容替换为另一个程序,并且永远不会返回。因此,exec 之后的子代码不会运行(请参阅文档)。因此$pm-&gt;finish 被挂起,子进程永远不会被收割,并且操作系统将其信息保存在进程表中,因此存在已失效/僵尸。

    这是直接使用exec 启动另一个程序的基本方法

    my $cmd = '...';
    
    my $pid = fork  // die "Can't fork: $!";
    
    if ($pid == 0) {
        exec $cmd;
        die "exec shouldn't return: $!";
    }
    my $gone = waitpid $pid, 0;
    
    if    ($gone > 0) { say "Child $gone exited with $?" }
    elsif ($gone < 0) { say "No $pid process ($gone), reaped already?" }
    else              { say "Process $pid still running?" }
    

    在这里,子级从父级继承标准流。此外,在某些情况下,错误报告很粗略(不精确),请参阅 cmets,感谢 ikegami。

    ikegami 的answer 可以更详细、更忠实地替代您所做的工作。

    【讨论】:

    • 这使得当exec 失败时看起来孩子成功启动。这就是为什么我推荐open3(或者更好的东西)。
    • @ikegami 这是我没有得到的——这样我看到的是,如果fork 失败,则会有一条消息,而如果我得到die,那么孩子 已成功创建。或者你的意思是waitpid 不会透露问题出在哪里?我怀疑我可能在这里遗漏了一些东西。 (顺便说一句,我并不是说这是一个完整而强大的方法,我希望帖子能清楚地说明这一点。)谢谢你的 cmets。
    • 您的方式无法区分启动失败sshssh 执行exit(2)。这不是 Perl 的 system 的工作方式。那不是 C 的 system 作品。 bash 不是这样工作的。在所有这些情况下,exec 失败和准备exec 的错误似乎是启动程序失败,而不是启动程序返回的错误。 (对于 Perl 的 system,它将返回 $? = -1 并设置 $!。)
    • @ikegami 好的,我明白你的意思了。我松散地意识到这里错误报告的粗糙(不精确),谢谢你。现在添加一个基本评论,一旦我得到它会做更多(或删除)。再次感谢您。
    • 我并不是说你需要改变任何东西;我只是指出一个限制。但这是在您之前 25 分钟发布的解决方案中解决的问题。
    猜你喜欢
    • 1970-01-01
    • 2018-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-14
    • 2017-08-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多