【问题标题】:Why does closing a pipe take so long to terminate a child process?为什么关闭管道需要这么长时间才能终止子进程?
【发布时间】:2015-01-26 21:38:07
【问题描述】:

我的程序在等待子进程 (gzip) 完成时遇到问题,并且需要很长时间才能完成。

在它开始等待之前,它会关闭gzip 的输入流,所以这应该会触发它很快终止。我检查了系统,gzip 没有消耗任何 CPU 或等待 IO(写入磁盘)。

非常奇怪的是它停止等待的时间......

我们在内部使用 pthreads 的程序。它并排处理 4 个 pthread。每个线程处理许多工作单元,对于每个工作单元,它启动一个新的gzip 进程(使用fork()execve())来写入结果。当gzip 没有终止时线程挂起,但是当其他线程关闭它们的实例时它突然终止。

为了清楚起见,我正在设置一个管道:my program(pthread) --> gzip --> file.gz

我想这可以部分解释为 CPU 负载。但是当进程间隔几分钟启动并且整个系统由于这个锁定问题而最终只使用 4 个核心时,这似乎不太可能。

启动gzip 的代码如下。调用execPipeProcess 以便孩子直接写入文件,但从我的程序中读取。那就是:

execPipeProcess(&process, "gzip", -1, gzFileFd)

有什么建议吗?

typedef struct {
    int processID;
    const char * command;
    int stdin;
    int stdout;
} ChildProcess;


void closeAndWait(ChildProcess * process) {
    if (process->stdin >= 0) {
                stdLog("Closing post process stdin");
                if (close(process->stdin)) {
                exitError(-1,errno, "Failed to close stdin for %s",  process->command);
                }
        }
    if (process->stdout >= 0) {
                stdLog("Closing post process stdin");
                if (close(process->stdout)) {
            exitError(-1,errno, "Failed to close stdout for %s", process->command);
                }
        }

    int status;
        stdLog("waiting on post process %d", process->processID);
    if (waitpid(process->processID, &status, 0) == -1) {
        exitError(-1, errno, "Could not wait for %s", process->command);
    }
        stdLog("post process finished");

    if (!WIFEXITED(status)) exitError(-1, 0, "Command did not exit properly %s", process->command);
    if (WEXITSTATUS(status)) exitError(-1, 0, "Command %s returned %d not 0", process->command, WEXITSTATUS(status));
    process->processID = 0;
}



void execPipeProcess(ChildProcess * process, const char* szCommand, int in, int out) {
    // Expand any args
    wordexp_t words;
    if (wordexp (szCommand, &words, 0)) exitError(-1, 0, "Could not expand command %s\n", szCommand);


    // Runs the command
    char nChar;
    int nResult;

    if (in < 0) {
        int aStdinPipe[2];
        if (pipe(aStdinPipe) < 0) {
            exitError(-1, errno, "allocating pipe for child input redirect failed");
        }
        process->stdin = aStdinPipe[PIPE_WRITE];
        in = aStdinPipe[PIPE_READ];
    }
    else {
        process->stdin = -1;
    }
    if (out < 0) {
        int aStdoutPipe[2];
        if (pipe(aStdoutPipe) < 0) {
            exitError(-1, errno, "allocating pipe for child input redirect failed");
        }
        process->stdout = aStdoutPipe[PIPE_READ];
        out = aStdoutPipe[PIPE_WRITE];
    }
    else {
        process->stdout = -1;
    }

    process->processID = fork();
    if (0 == process->processID) {
        // child continues here

        // these are for use by parent only
        if (process->stdin >= 0) close(process->stdin);
        if (process->stdout >= 0) close(process->stdout);

        // redirect stdin
        if (STDIN_FILENO != in) {
            if (dup2(in, STDIN_FILENO) == -1) {
              exitError(-1, errno, "redirecting stdin failed");
            }
            close(in);
        }

        // redirect stdout
        if (STDOUT_FILENO != out) {
            if (dup2(out, STDOUT_FILENO) == -1) {
              exitError(-1, errno, "redirecting stdout failed");
            }
            close(out);
        }

        // we're done with these; they've been duplicated to STDIN and STDOUT

        // run child process image
        // replace this with any exec* function find easier to use ("man exec")
        nResult = execvp(words.we_wordv[0], words.we_wordv);

        // if we get here at all, an error occurred, but we are in the child
        // process, so just exit
        exitError(-1, errno, "could not run %s", szCommand);
  } else if (process->processID > 0) {
        wordfree(&words);
        // parent continues here

        // close unused file descriptors, these are for child only
        close(in);
        close(out);
        process->command = szCommand;
    } else {
        exitError(-1,errno, "Failed to fork");
    }
}

【问题讨论】:

  • 也许你正在耗尽进程限制或类似的东西?
  • 不在 4 pthreads + 4 procecsses 我不是。不过谢谢你的建议。我已经通过调试器检查了这一点,它在等待时停止,而不是在 fork 上。无论如何感谢您的建议。
  • “每个线程处理许多工作单元,并且对于每个工作单元,它都会启动一个新的 gzip 进程”每个 what ??每一个什么?线程还是“多个工作单元”?而在 pthread 多线程应用程序中间的fork()-ing 并不完全可取。 See here。它是可行的,但你必须小心。
  • 抱歉...每个工作单元都有自己的 gzip 进程,每个线程有很多 gzip 进程,但每个线程只能同时有一个。
  • 请注意,如果in == STDIN_FILENO 和/或out == STDOUT_FILENO,则可能不是您希望孩子这样做:“close(in); close(out);”。

标签: c pthreads pipe fork


【解决方案1】:

子进程继承打开的文件描述符。

每个后续的 gzip 子进程不仅继承用于与该特定实例通信的管道文件描述符,还继承连接到先前子进程实例的管道的文件描述符。

这意味着当主进程执行关闭时标准输入管道仍然打开,因为在几个子进程中同一管道还有一些其他文件描述符。一旦这些终止,管道最终关闭。

一个快速的解决方法是通过设置 close-on-exec 标志来防止子进程继承用于主进程的管道文件描述符。

由于涉及多个线程,因此应序列化生成子进程,以防止子进程继承用于另一个子进程的管道 fd。

【讨论】:

  • 子进程从父进程继承打开文件描述符是正确的。此外,文件描述符是进程范围的,而不是每个线程的。尽管如此,从发布的代码中并不清楚该程序是否存在您描述的问题。这取决于如何使用所提供的功能。
  • 谢谢尼克,这就是问题所在。 close-on-exec 标志解决了这个问题。由于 pthread 不共享这些输出进程或文件句柄,因此这是代码确保及时关闭这些句柄的唯一方法。
【解决方案2】:

您没有向我们提供足够的信息来确定,因为答案取决于您如何使用所提供的功能。但是,您的 closeAndWait() 函数看起来有点可疑。假设有问题的子进程将在到达其stdin 的末尾时退出可能是合理的,但是它已经写入甚至可能仍然写入其stdout 的数据应该发生什么?您的子进程可能会挂起,因为它们的标准输出被阻塞,并且它们识别它的速度很慢。

我认为这反映了一个设计问题。如果您正在捕获子进程的输出,至少您似乎支持这样做,那么在关闭子进程输入流的父进程结束后,您将希望父进程继续读取子进程的输出,并执行任何操作处理它打算对其进行的处理。否则你可能会丢失一些数据(对于执行gzip 的孩子来说,这意味着数据损坏)。如果您将关闭两个流作为终止子进程的一部分,则不能这样做。

相反,您应该首先关闭子stdin 的父端,继续处理其输出直到到达其末尾,然后才尝试收集子端。如果您愿意,可以将关闭子输出流的父级结束作为收集该子级的过程的一部分。或者,如果您确实想丢弃子节点的任何剩余输出,那么您应该在关闭输入和关闭输出之间排出其输出流。

【讨论】:

  • 感谢约翰的建议。恐怕这不能解释时间。碰巧我正在使用代码来设置 gzip 以直接写入文件(而不是通过我自己的程序循环返回)。
  • 很公平,它不能解释你的问题,但它肯定构成a问题。
  • 是的,我明白你的意思,这个函数的用例非常具体。我可以看到它可能会出错的多种方式。我会将重构添加到我的 TODO 列表中,以保护我自己免受我自己的代码的影响 :-)
猜你喜欢
  • 1970-01-01
  • 2019-11-13
  • 1970-01-01
  • 1970-01-01
  • 2012-04-29
  • 2020-02-15
  • 1970-01-01
  • 1970-01-01
  • 2017-09-22
相关资源
最近更新 更多