【问题标题】:Why aren't I picking up the exit status from my child process?为什么我没有从我的子进程中获取退出状态?
【发布时间】:2013-09-05 15:46:59
【问题描述】:

我有一个我正在管理的 Perl 程序,它能够分叉多个进程(达到指定的限制)、监视它们,并在它们退出时分叉其他进程(再一次,直到限制),直到要运行的事物列表完成。它工作正常,但由于某种原因,它似乎没有从我的子进程中获取正确的退出状态。

不起作用的代码使用Perl的fork()waitpid(),子进程使用POSIX::_exit()退出。以下是相关代码的部分摘录:

分叉代码:

# Initialize process if running in parallel mode
my $pid;
if ($options{'parallel'} > 0) {
    log_status("Waiting to fork test #".$curr_test{'id'}."...\n");

    # Here, wait for child processes to complete so we can fork off new ones without going over the specified limit
    while ( keys(%children) >= $options{'parallel'}) {
        my $kid = waitpid(-1, 0);
        my $kid_status = $?;

        if ($kid > 0) {
            log_status("Child process (PID ".$kid.", test ".$children{$kid}.") exited with status ".$kid_status.".\n");
            $error_status |= $kid_status;
            delete $children{$kid};
        }
    }

    $pid = fork();
    tdie("Unable to fork!\n") unless defined $pid;

    if ($pid != 0) {
        # I'm the parent
        $is_child = 0;
        log_status("Forked child process (PID ".$pid.").\n");

        $children{$pid} = $curr_test{'logstr'};

        next TEST_LOOP;
    }
    else {
        # I'm the child
        $is_child = 1;
        log_status("Starting test = ".$curr_test{'logstr'}."\n");
    }
}

退出子进程代码:

### finish_child() ###
# Handles exiting the script, like the finish() function, but only when running as a child process in parallel mode.
# Parameters:
#   - The error code to exit with
###
sub finish_child( $ ) {
    my ($error_status) = @_;


    # If running in parallel mode, exit this fork
    if ($options{'parallel'} > 0) {
        log_status("Entering: ".Cwd::abs_path("..")."\n");
        chdir "..";
        log_status("Exiting with status: ".$error_status."\n");
        POSIX::_exit($error_status);
    }
}

这是在我的示例运行中调用 finish_child() 的地方:

# If build failed, log status and gracefully clean up logfiles, then continue to next test in list.
if ($test_status > 0) {
    $email_subject = "Build failed!";
    log_status("Build of ".$testline." FAILED.\n");
    tlog(1, "Build of ".$testline." FAILED.\n");

    log_status("Entering: ".Cwd::abs_path("..")."\n");
    chdir "..";


    log_report(\%curr_test, $test_status);

    # Print out pass/fail status for each test as it completes
    $quietmode = $options{'quiet'}; # Backup quiet mode setting
    $options{'quiet'} = 0;

    if ($test_status == 0) {
        log_status("Test ".$testline." PASSED.\n");
        tlog(0, "Test ".$testline." PASSED.\n");
    }
    else {
        log_status("Test ".$testline." FAILED.\n");
        tlog(1, "Test ".$testline." FAILED.\n");
    }

    $options{'quiet'} = $quietmode;  # Restore quiet mode setting
    finish_logs();


    # Link logs to global area and rename if running multiple tests
    system("ln -sf ".$root_dir."/verify/".$curr_test{'id'}."/".$verify::logfile." ../".(($test_status > 0) ? "fail".$curr_test{'id'}.".log" : "pass".$curr_test{'id'}.".log" )) if (@tests > 1);


    if ($options{'parallel'} > 0 && $pid == 0) {
        # If we're in parallel mode and I'm a child process, I should exit, instead of continuing to loop.
        finish_child($test_status);
    }
    else {
        # If we're not in parallel mode, I should continue to loop.
        next TEST_LOOP;
    }
}

这是我根据运行日志看到的行为:

<Parent> Waiting for all child processes to complete...
<Child> [PID 28657] Entering: <trimmed>
<Child> [PID 28657] Running user command: make --directory <trimmed> TARGET=build BUILD_DIR=<trimmed> RUN_DIR=<trimmed>            
<Child> [PID 28657] User command finished with return code: 512
<Child> [PID 28657] Build step finished with return code 512
<Child> [PID 28657] Entering: <trimmed>
<Child> [PID 28657] Build of rx::basic(1) FAILED.
<Child> [PID 28657] Entering: <trimmed>
<Child> [PID 28657] Test rx::basic(1) FAILED.
<Child> [PID 28657] Closing log file.
<Child> [PID 28657] Closing error log file.
<Child> [PID 28657] Entering: <trimmed>
<Parent> Child process (PID 28657, test rx::basic(1)) exited with status 0.

我有使用 Perl IPC 来运行命令的代码(代替 system() 调用,以便更灵活地正确选择退出代码,您可以在日志文件的“用户命令”行中看到。

我可能做错了什么,在这里?在这种情况下,为什么我无法从$? 获取退出状态?我在网上找到的示例似乎都表明这应该可以正常工作。

作为参考,我正在运行Perl v5.10.1。如果您觉得需要查看其余代码,这个 Perl 工具也在 GitHub 上开源:https://github.com/benrichards86/Verify/blob/master/verify.pl

【问题讨论】:

  • 日志中的“Exiting with status:”行在哪里?
  • @ikegami 这个我不确定。我之前没有注意到这一点。可能是它在缓冲区中丢失了。我会进一步调试这个特定的东西,看看我发现了什么。
  • 啊,我运行的代码版本在finish_child() 中没有那个特定的日志记录语句,这就是为什么它没有出现在我上面发布的日志 sn-p 中的原因。不过,这是唯一的区别。因此出现差异。对此感到抱歉。

标签: perl posix fork


【解决方案1】:

如果$test_status 是 512,你打电话给POSIX::_exit(512)?这是不正确的。 子进程应该调用POSIX::_exit,操作数在0 到255 范围内,获取该子进程的Perl 父进程将$? 设置为exit-status &lt;&lt; 8

POSIX::_exit(512) 等价于POSIX::_exit(512 % 256),或POSIX::_exit(0)

【讨论】:

  • 哦,这是一个很好的观点,而不是我接受的东西。我将对此进行测试,看看它是否有效!
  • 我尝试了修复,它成功了!将此标记为答案(尽管这里的每个答案都很有帮助)。
  • @ikegami 我使用了你的“像 bash 一样做”的建议并打电话给:POSIX::_exit(($error_status &amp; 0x7F) ? ($error_status | 0x80) : ($error_status &gt;&gt; 8));。将此标记为答案,因为他先到这里并且是正确的。
【解决方案2】:

看来你正在做的事情相当于:

exit($?)

您的意思是将孩子传递给exit 的值传播,但这不是$? 包含的内容。

如果子进程被信号杀死,$? &amp; 0x7F 包含杀死进程的信号编号。

如果子进程没有被信号杀死,$? &amp; 0x7F 为零,$? &gt;&gt; 8 包含进程传递给exit 的值。

所以当孩子执行exit(1) 时,您执行exit(256),这超出了 Unix 系统的范围。高位被切掉,留下零 (256 &amp; 0xFF = 0)。


我建议你做bash 所做的事情:

exit( ($? & 0x7F) ? ($? | 0x80) : ($? >> 8) );

当孩子做exit(1)时,这会做exit(1)

当孩子被 SIGTERM (15) 杀死时,exit(128 + 15).

【讨论】:

  • (我在示例中使用了exit,但我所说的一切也适用于_exit。)
  • 谢谢!我在调用POSIX::_exit() 的地方使用了您的“做bash 所做的事情”的建议,从而解决了这个问题。不过,我标记了@mob 的答案,因为他先到了这里。 :)
【解决方案3】:

是的,这可能是解释,但让我感兴趣的是您的测试输出没有显示孩子实际使用的退出状态。代码中有一条日志消息(“Exiting with status:...”),但输出中没有相应的行。

因此,我们无法确定您的这部分代码是否有任何问题。

我最初认为使用 POSIX::_exit 可能会解释日志记录问题(它会阻止最终缓冲区被刷新),但再次查看您的代码,我发现您在调用 finish_child 之前已经关闭了日志记录。

我建议您首先让日志记录正常工作,这样您就可以知道问题出在哪里。为什么不将日志关闭和日志文件重命名逻辑移到完成子例程中,作为退出前完成的最后一件事?

关于退出状态问题,我看到了三种可能的解释,都在子进程的代码中:

  • 孩子实际上并未通过函数 finish_child 退出
  • 您认为传递给 finish_child 然后退出的非零状态实际上并未传递
  • 如上所述,您的退出状态为 > 255

您使用 POSIX::_exit() 代替 exit() 和 waitpid(-1) 代替 wait() 有什么特殊原因吗?

【讨论】:

  • 我一直在重构代码。这主要集中在解析/索引上,但脚本的主要流程也可能会使用返工,这最终会包括日志记录。如果那里有一些日志记录错误,我不会感到惊讶! :)
  • _exit 不会调用要在父级中执行的析构函数和 END 块。例如,如果您在父级中有一个数据库句柄,则在子级中调用exit 将关闭它,但_exit 将使父级不受影响。
  • @ikegami 是的,这就是我使用它的原因。我相信我一开始尝试了exit() 并遇到了这个问题(一个是陈旧的文件句柄)。
  • 如果您希望您的 END 块根据您是否是孩子做不同的事情,请使用全局“$is_child”变量,您在输入孩子代码时将其设置为第一件事.这样您就可以使用标准的 exit() 函数,这意味着您不必担心使用非核心函数的任何副作用。
  • @VolDeNuit 我可以看看。我昨天意识到,我可能可以在比当前更多的地方使用该变量。正如我所说,我正在修改很多代码,所以这可能是我要研究的内容。
猜你喜欢
  • 1970-01-01
  • 2020-02-27
  • 2021-08-09
  • 2011-08-04
  • 1970-01-01
  • 1970-01-01
  • 2018-06-24
  • 1970-01-01
  • 2015-02-03
相关资源
最近更新 更多