孩子离开后管道保证关闭

时间:2018-10-17 15:45:42

标签: c pipe fork dup2

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

#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()可能挂起吗?

谢谢!

1 个答案:

答案 0 :(得分:1)

关于您的想法:

  • 如果子代产生自己的子代,则read()直到其所有后代死亡或关闭stdout和stderr时才返回0。如果不是这样,或者如果孩子总是比所有后代都寿命更长,那么仅等待read()返回0就足够了,永远不会造成问题。
  • 如果孩子死了,但是父母还没有wait(2),那么kill(pid, 0)将会成功,就好像孩子还活着(至少在Linux上一样),所以这不是在您的父程序中进行有效的检查。
  • 无障碍waitpid()本身似乎可以解决孩子拥有自己的孩子的问题,但实际上会引入一种微妙的比赛条件。如果孩子在waitpid()之后但在read()之前退出,那么read()将会阻塞,直到其他后代退出。
  • 如果单独使用select(),那无非就是调用read()更好。如果您以非阻塞方式使用select(),那么您最终将在循环中消耗CPU时间。

我会做什么:

  • 为SIGCHLD添加一个无操作信号处理程序功能,以使它在发生EINTR时产生。
  • 在开始循环之前,将SIGCHLD阻止在父级中。
  • 使用非阻塞read,然后使用pselect(2)进行阻塞以避免永远旋转CPU。
  • pselect期间,传递未阻止SIGCHLD的sigset_t,这样可以保证在最终发送该证书时引起EINTR。
  • 在循环中的某个位置执行非阻塞waitpid(2),并适当地处理其返回。 (请确保在阻止SIGCHLD之后但第一次调用select之前,至少要执行一次此操作,否则会出现竞争状况。)