【问题标题】:Pipe guarantee to close after the child has exited管道保证在孩子退出后关闭
【发布时间】:2018-10-17 15:45:42
【问题描述】:

在下面的代码中,依靠 read() failure 来检测孩子的终止是否安全?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
   int pipefd[2];
   pipefd[0] = 0;
   pipefd[1] = 0;
   pipe(pipefd);

   pid_t pid = fork();

   if (pid == 0)
   {
      // child
      close(pipefd[0]);    // close unused read end
      while ((dup2(pipefd[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {}             // send stdout to the pipe
      while ((dup2(pipefd[1], STDERR_FILENO) == -1) && (errno == EINTR)) {} // send stderr to the pipe
      close(pipefd[1]);    // close unused write end

      char *argv[3];
      argv[0] = "worker-app";
      argv[1] = NULL;
      argv[2] = NULL;
      execvp("./worker-app", argv);
      printf("failed to execvp, errno %d\n", errno);
      exit(EXIT_FAILURE);
   }
   else if (pid == -1)
   {
   }
   else
   {
      // parent
      close(pipefd[1]);  // close the write end of the pipe in the parent

      char buffer[1024];
      memset(buffer, 0, sizeof(buffer));
      while (1) // <= here is it safe to rely on read below to break from this loop ?
      {
        ssize_t count = read(pipefd[0], buffer, sizeof(buffer)-1);
        printf("pipe read return %d\n", (int)count);
        if (count > 0)
        {
          printf("child: %s\n", buffer);
        }
        else if (count == 0)
        {
          printf("end read child pipe\n", buffer);
          break;
        }
        else if (count == -1)
        {
          if (errno == EINTR)
          {   continue;
          }
          printf("error read child pipe\n", buffer);
          break;
        }
      }

      close(pipefd[0]); // close read end, prevent descriptor leak

      int waitStatus = 0;
      waitpid(pid, &waitStatus, 0);
  }

  fprintf(stdout, "All work completed :-)\n");
  return EXIT_SUCCESS;
}

我应该在 while(1) 循环中添加一些东西来检测子终止吗?可能会发生什么特定情况并破坏此应用程序?

下面的一些改进想法。但是我会浪费 CPU 周期吗?

  1. 使用带有特殊参数 0 的 kill 不会终止进程,而只是检查它是否响应: if (kill(pid, 0)) { break; /* child exited */ }; /* 如果 sig 为 0,则不发送信号,但仍进行错误检查;这可用于检查进程 ID 或进程组 ID 是否存在。 https://linux.die.net/man/2/kill */

  2. 在 while(1) 循环中使用 waitpid 非阻塞来检查子进程是否已退出。

  3. 使用 select() 检查管道可读性以防止 read() 可能挂起?

谢谢!

【问题讨论】:

    标签: c pipe fork dup2


    【解决方案1】:

    关于你的想法:

    • 如果子代产生自己的子代,read() 将不会返回 0,直到其所有子代都死亡或关闭 stdout 和 stderr。如果不是这样,或者如果孩子总是比它的所有后代活得更久,那么只需等待 read() 返回 0 就足够了,而且不会造成任何问题。
    • 如果孩子死了但父母还没有wait(2)ed,那么kill(pid, 0) 将成功,就好像孩子还活着一样(至少在 Linux 上),所以这不是一个有效的检查在您的父项目中。
    • 非阻塞waitpid() 本身似乎可以解决孩子拥有自己的孩子的问题,但实际上会引入微妙的竞争条件。如果孩子在waitpid() 之后但在read() 之前退出,则read() 将阻塞,直到其余后代退出。
    • 如果您以阻塞方式使用select(),就其本身而言,这并不比仅调用read() 更好。如果你以非阻塞方式使用select(),你最终只会在循环中消耗 CPU 时间。

    我会做什么:

    • 为 SIGCHLD 添加一个无操作信号处理函数,以便在它发生时引起 EINTR。
    • 在开始循环之前阻止父级中的 SIGCHLD。
    • 使用非阻塞 reads,并使用 pselect(2) 阻塞以避免 CPU 永远旋转。
    • pselect 期间,传入一个没有阻止 SIGCHLD 的sigset_t,这样就可以保证在最终发送它时为其生成一个 EINTR。
    • 在循环中的某处,执行非阻塞waitpid(2),并适当处理其返回。 (请确保在阻止 SIGCHLD 之后但在第一次调用 select 之前至少执行一次此操作,否则您将遇到竞争条件。)

    【讨论】:

    • 感谢您的回答!您确定“使用非阻塞读取,并使用 pselect(2) 阻塞以避免永远旋转 CPU。”。在我的基准测试中,阻塞 read() 对性能没有明显影响。
    • 您所描述的使用 SIGCHLD 可以防止哪些情况?
    • @theconstantgardener 关于 CPU 使用率,“永远旋转”的情况只有在没有 pselect 的情况下使用非阻塞读取时才会发生,这是我所警告的。就 CPU 而言,阻塞读取会很好。
    • @theconstantgardener Re SIGCHLD,这个想法是确保 EINTR 碰巧让您的程序在孩子死亡时退出 select 调用。
    猜你喜欢
    • 2017-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-04
    相关资源
    最近更新 更多