与 C 中读取的子进程的通信

Comunication to from child process hanging on read in C

我正在尝试与外部程序通信,如果执行该程序,将 运行 一个终端界面。 通常我必须提供一些输入(例如“1+1”),然后读取程序的输出(例如“2”)。 由于我需要双向通信,所以我无法使用 popen()。

我的问题如下:

每当我有一部分代码要求输入时,例如包含 std::cin >> input 我 运行 进入同一问题,read 命令永远不会退出。

这里我写了一个最小的例子,子进程所做的就是读取输入并重复它。

当我尝试 运行 这段代码时,我看到了第一个打印“Parent says:”,我可以提供输入并使用 write 发送它。但是,当我尝试再次调用 read() 函数并查看结果时,它永远不会退出。

我注意到,如果我关闭从父级到子级的管道 (fd_p2c[1]),那么我可以成功读取。 这显然不是我想要的,因为在我的应用程序中我想保持两个通信都打开。

关于如何解决这个问题有什么建议吗?

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

int main() {
  int status, buf_length;

  // Input and output
  char buf[256];
  char msg[256];
  char child_read[256];

  int fd_c2p[2]; // file descriptor pipe child -> parent
  int fd_p2c[2]; // file descriptor pipe parent -> child

  pipe(fd_c2p);
  pipe(fd_p2c);

  // Spawn a new process with pid
  pid_t pid = fork(); // Fork process

  if (pid == 0) {
    // Child

    // Close the unused end of the pipe
    if (close(fd_p2c[1]) != 0 || close(fd_c2p[0]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Set the comunication
    if (dup2(fd_p2c[0], STDIN_FILENO) != 0 ||
        dup2(fd_c2p[1], STDOUT_FILENO) != 1 ||
        dup2(fd_c2p[1], STDERR_FILENO) != 2) {
      fprintf(stderr, "Faild to duplicate the end of the pipes\n");
      exit(1);
    }

    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // ask kernel to deliver SIGTERM in case the parent dies
    prctl(PR_SET_PDEATHSIG, SIGTERM);

    // Moch program
    while (1) {
      fprintf(stdout, "Parent says: ");
      fflush(stdout);
      scanf("%s", child_read);
      fprintf(stdout, " >> Child repeat: %s\n", child_read);
      fflush(stdout);
    }
    exit(1);
  } else {
    // Parent

    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }
  }

  // Read output and send input
  while (1) {
    // Read from child
    while (buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1)) {
      buf[buf_length] = '[=10=]';
      printf("%s", buf);
    }

    // Enter message to send
    scanf("%s", msg);
    if (strcmp(msg, "exit") == 0)
      break;

    // Send to child
    write(fd_p2c[1], msg, strlen(msg));
    //close(fd_p2c[1]);
  }

  printf("KILL");
  kill(pid, SIGKILL); // send SIGKILL signal to the child process
  waitpid(pid, &status, 0);
}

如果你需要双向通信,你可以使用一些unix(7) socket, or several pipe(7)-s, or some fifo(7)

您可以使用一些 JSONRPC library. Or XDR (perhaps ONC/RPC/XDR) or ASN/1 for binary communication between heterogeneous computers in a data center, or MPI. If you can use some supercomputer,它可能有专有库来简化不同节点上进程 运行 之间的消息传递。

考虑使用 OpenMPI

I'm trying to communicate with an external program which, if executed, will run a terminal interface.

也许您需要一些 pty(7) with termios(3)?然后从 xtermrxvt

的源代码中获取灵感

你可能需要一些 event loop around a poll(2) before attempting a read(2) (or recv(2)...) or a write(2) (or send(2))

您可以找到开源库(例如 Glib, libev、...)来帮助您,您当然应该研究它们的源代码以获取灵感。

子进程中有一个问题:

scanf("%s", child_read);

对于 %s 格式,只有三件事可以阻止 scanf 等待更多输入:

  1. 错误
  2. 文件结束
  3. Space

假设没有出错,就不会有错误。由于父进程保持管道打开,因此不会有文件结尾。由于父进程只写它自己用 scanf("%s", ...) 读取的内容,因此发送的数据中不会有空格。

总而言之,子进程将无限期地等待 scanf 到 return,它永远不会。

我们称单词scanf("%s")能够提取的内容。 这是一个连续的字符序列,不是分隔符(space、制表符、new-line...)。

child 的(重定向的)标准输入读取 wordscanf("%s", child_read);。 当读取分隔符或到达 EOF 时,此 word 被称为结束。 在 parent 中,write(fd_p2c[1], msg, strlen(msg)); 发送了一个 (紧接着就没有了)因为 msg 在之前被提取为 .

请注意,当您使用键盘输入 字词 时,您还会按下回车键,这会在标准输入中发送 new-line 分隔符。此时,终端使这一行可用于 scanf()word 被称为结束并且分隔符被忽略(在这种特定情况下,但我们可以通过fgetc()).

例如,如果在parent的标准输入中我们输入"abc\n", parent 获得单词 "abc" 并按原样发送给 child。 然后 child 收到一个以 "abc" 开头但尚未结束的单词: scanf("%s") 仍在等待 c 之后的一些其他字符以使该单词更长或分隔符或 EOF 到检测这个词的结尾。

例如,您可以在该词后发送一个分隔符。

// Send to child
write(fd_p2c[1], msg, strlen(msg));
char lf='\n';
write(fd_p2c[1], &lf, 1);

或者依靠 fgets()(而不是 scanf("%s"))在 child 和 parent.

顺便说一下,parent 中的 while/read 我觉得很奇怪。 我会做这样的事情。

// Read from child
buf_length = (int)read(fd_c2p[0], buf, sizeof(buf) - 1);
if (buf_length <= 0) {
  break;
}

因为一般来说,我不知道消息的长度,因为我需要阅读表格 fd_c2p,我需要创建一个监听管道直到它为空的外观。

要做到这一点,有必要按照@some-programmer-dude 的建议将 O_NONBLOCK 添加到父文件描述符中:

    // Parent

    // close unused pipe ends
    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Add O_NONBLOCK the the fd that reads from the child
    int c2p_flags = fcntl(fd_c2p[0], F_GETFL);
    fcntl(fd_c2p[0], F_SETFL, c2p_flags | O_NONBLOCK);

现在,当我从文件描述符 fd_c2p[0] 读取子文件的输出时,每当我们尝试从空文件读取时,它 returns 就会出错。 读取 errno 中的错误代码应该匹配 EWOULDBLOCK.

要知道何时停止阅读 fd_c2p[0] 需要一些关于输出的知识。 当该行的最后一个字符到达 EWOULDBLOCK 且上一条消息以 :.

结束时,此特定阅读应停止
    // Read from child
    end_of_message = false;
    while (1) {
      buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1);
      if (buf_length == -1)
      {
        if (end_of_message && errno == EWOULDBLOCK)
          break;
        else if (errno == EWOULDBLOCK)
          continue;
        else {
          fprintf(stderr, "reading from pd_c2p returned an error different "
                          "from `EWOULDBLOCK'\n");
          exit(errno);
        }
      }
      buf[buf_length] = '[=11=]';
      printf("%s", buf);
      end_of_message = buf[buf_length - 1] == ':';
    }

此补丁解决了在程序要求输入之前不确定有多少行时从文件读取的问题。

当消息在任何位置包含:时也应该是安全的。为了测试它,可以将不同的缓冲区减少到更小的大小(例如从 256 到 1)。

@prog-fh 也指出,原则上人们也希望输入也包含空格。容纳可以使用 fgets 而不是 scanf:

   // Enter message and send it over to the chid process
    while (fgets(msg, 256, stdin) != NULL) {
      if (msg[strlen(msg)] == '[=12=]')
        write(fd_p2c[1], msg, strlen(msg));
      else {
        fprintf(stderr, "Error encounter while reading input\n");
        exit(1);
      }

      if (msg[strlen(msg) - 1] == '\n')
        break;
      else
        continue;
    }

使用 fgets 的优点之一是字符串将在末尾保留换行符 \n,这意味着无需将额外的字符推送到写入缓冲区一旦我们读完消息。

完整代码如下

#include <cerrno>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
  int status, buf_length;
  bool end_of_message = false;

  int fd_c2p[2]; // file descriptor pipe child -> parent
  int fd_p2c[2]; // file descriptor pipe parent -> child

  // Input and output
  char buf[256];
  char msg[256];
  char child_read[256];

  // We need two pipes if we want a two way comunication.
  pipe(fd_c2p);
  pipe(fd_p2c);

  // Spawn a new process with pid
  pid_t pid = fork(); // Fork process

  if (pid == 0) {
    // Child

    // Close the unused end of the pipe
    if (close(fd_p2c[1]) != 0 || close(fd_c2p[0]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Set the comunications
    if (dup2(fd_p2c[0], STDIN_FILENO) != 0 ||
        dup2(fd_c2p[1], STDOUT_FILENO) != 1 ||
        dup2(fd_c2p[1], STDERR_FILENO) != 2) {
      fprintf(stderr, "Faild to duplicate the end of the pipes\n");
      exit(1);
    }

    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // ask kernel to deliver SIGTERM in case the parent dies
    prctl(PR_SET_PDEATHSIG, SIGTERM);

    // Moch Program
    while (1) {
      fprintf(stdout, "Parent says:");
      fflush(stdout);

      fgets(child_read, 256, stdin);
      fprintf(stdout, " >> Child repeat: %s", child_read);
      while (child_read[strlen(child_read) - 1] != '\n') {
        fgets(child_read, 256, stdin);
        fprintf(stdout, " >> Child repeat: %s", child_read);
      }
      fflush(stdout);
    }

    // Nothing below this line should be executed by child process.
    // If so, it means that thera has beed a problem so lets exit:
    exit(1);
  } else {
    // Parent

    // close unused pipe ends
    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Add O_NONBLOCK the the fd that reads from the child
    int c2p_flags = fcntl(fd_c2p[0], F_GETFL);
    fcntl(fd_c2p[0], F_SETFL, c2p_flags | O_NONBLOCK);
  }

  // Now, you can write to fd_p2c[1] and read from fd_c2p[0] :
  while (1) {

    // Read from child
    end_of_message = false;
    while (1) {
      buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1);
      if (buf_length == -1)
      {
        if (end_of_message && errno == EWOULDBLOCK)
          break;
        else if (errno == EWOULDBLOCK)
          continue;
        else {
          fprintf(stderr, "reading from pd_c2p returned an error different "
                          "from `EWOULDBLOCK'\n");
          exit(errno);
        }
      }
      buf[buf_length] = '[=13=]';
      printf("%s", buf);
      end_of_message = buf[buf_length - 1] == ':';
    }

    // Enter message and send it over to the chid process
    while (fgets(msg, 256, stdin) != NULL) {
      if (msg[strlen(msg)] == '[=13=]')
        write(fd_p2c[1], msg, strlen(msg));
      else {
        fprintf(stderr, "Error encounter while reading input\n");
        exit(1);
      }

      if (msg[strlen(msg) - 1] == '\n')
        break;
      else
        continue;
    }

    // Check if the user wants to exit the program
    if (strcmp(msg, "exit\n") == 0)
      break;
  }

  printf("KILL");
  kill(pid, SIGKILL); // send SIGKILL signal to the child process
  waitpid(pid, &status, 0);
}