Связь с дочерним процессом, зависшим при чтении в C

Я пытаюсь связаться с внешней программой, которая, если она будет выполнена, запустит терминальный интерфейс. Обычно мне нужно предоставить некоторые входные данные (например, 1 + 1), а затем прочитать вывод программы (например, 2). Поскольку мне нужна двусторонняя связь, я не смог использовать popen().

Моя проблема заключается в следующем:

Всякий раз, когда у меня есть часть кода, которая запрашивает ввод, например, содержащая std::cin >> input, я сталкиваюсь с той же проблемой, команда read никогда не завершается.

Здесь я написал минимальный пример, все, что делает дочерний процесс, это читает ввод и повторяет его.

Когда я пытаюсь запустить этот код, происходит то, что я вижу первую печать Родитель говорит: и я могу предоставить ввод и отправить его, используя 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] = '\0';
      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);
}

person Snaporaz    schedule 03.02.2021    source источник
comment
Цикл while (buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1)) имеет один фатальный недостаток: если read не работает, вы не обнаружите его и сразу же запишете за пределы массива buf.   -  person Some programmer dude    schedule 03.02.2021
comment
Какое приложение вы разрабатываете?   -  person Basile Starynkevitch    schedule 04.02.2021
comment
@BasileStarynkevitch Извлекает информацию из набора дифференциальных уравнений, чтобы делать прогнозы некоторых процессов рассеяния в физике элементарных частиц. Все операции выполняются кодом C++, а алгебраические манипуляции передаются внешней программе.   -  person Snaporaz    schedule 04.02.2021
comment
@Someprogrammerdude Я почему-то думал, что отрицательные числа будут интерпретированы как ложные .... что, конечно, не так.   -  person Snaporaz    schedule 04.02.2021
comment
@snaporaz: несколько коллег из www-list.cea.fr могут заинтересоваться ..... И даже больше от cea.fr/english (мой нынешний работодатель), например. cea.fr/english/Pages/research-areas/ материя-и-вселенная.aspx - и, очевидно, ученые из home.cern   -  person Basile Starynkevitch    schedule 04.02.2021
comment
относительно: dup2(fd_c2p[1], STDOUT_FILENO) != 1 || dup2(fd_c2p[1], STDERR_FILENO) != 2) { похоже, это ошибка   -  person user3629249    schedule 06.02.2021


Ответы (4)


Одна проблема заключается в дочернем процессе:

scanf("%s", child_read);

В формате %s есть только три вещи, которые остановят scanf от ожидания дополнительных входных данных:

  1. Ошибка
  2. Конец файла
  3. Пространство

Если ничего не пойдет не так, ошибок не будет. А поскольку родительский процесс держит канал открытым, конца файла не будет. А так как родительский процесс пишет только то, что он сам читает с помощью scanf("%s", ...), то в отправляемых данных не будет пробелов.

В общем, дочерний процесс будет бесконечно ждать возврата scanf, чего он никогда не сделает.

person Some programmer dude    schedule 03.02.2021
comment
Дочерний процесс написан таким образом только для того, чтобы имитировать поведение, которое у меня есть, когда я выполняю реальную программу в дочернем процессе. Проблема в том, что я никогда не могу получить доступ к функции записи, потому что чтение зависает навсегда (как только ребенок запрашивает ввод). Я думаю, что это проблема с чтением, так как канал остается открытым (как и должно быть). Тогда возникает вопрос: есть ли способ проверить, пуст ли дескриптор файла, который я хочу прочитать? - person Snaporaz; 04.02.2021
comment
@Snaporaz Вы можете сделать дескрипторы неблокирующими. Тогда read вернется с ошибкой EWOULDBLOCK, если читать нечего. - person Some programmer dude; 04.02.2021
comment
Кажется, это помогает в сочетании со способом синхронизации связи. Я опубликую обновленный код со всей информацией. - person Snaporaz; 04.02.2021

Давайте назовем слово, что scanf("%s") может извлечь. Это непрерывная последовательность символов, не являющихся разделителями (пробел, табуляция, новая строка...).

Стандартный (перенаправленный) ввод дочернего элемента читает слово с scanf("%s", child_read);. Это слово считается оконченным, когда прочитан разделитель или достигнут EOF. В родительском элементе write(fd_p2c[1], msg, strlen(msg)); отправляет слово (и больше ничего сразу после него), потому что msg извлекается непосредственно перед ним как слово.

Обратите внимание, что когда вы вводите слово с клавиатуры, вы также нажимаете клавишу ввода, которая отправляет разделитель новой строки в стандартном вводе. В это время терминал делает эту строку доступной для scanf(), слово считается оконченным, а разделитель игнорируется (в этой конкретной ситуации, но мы могли бы получить его с помощью fgetc()).

Например, если в стандартный ввод родителя мы вводим "abc\n", родитель получает слово "abc", которое отправляется дочернему элементу как есть. Затем ребенок получает слово, начинающееся с "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")) для получения строки (а не просто слова) как в дочернем, так и в родительском.

Кстати, while/read в родительском элементе мне кажется странным. Я бы сделал что-то вроде этого.

// Read from child
buf_length = (int)read(fd_c2p[0], buf, sizeof(buf) - 1);
if (buf_length <= 0) {
  break;
}
person prog-fh    schedule 03.02.2021
comment
Действительно, мне нужно закончить отправленное сообщение, хороший улов! У меня все еще остается проблема с чтением, как-то я должен проверить, что дескриптор файла, из которого я читаю, пуст, и в этом случае я не должен пытаться читать, если я не ожидаю дополнительный вывод (например, я знаю, что до запрашивая ввод, программа печатает ›). - person Snaporaz; 04.02.2021

Если вам нужна двусторонняя связь, вы можете использовать unix(7 ) сокет или несколько pipe(7)-s или какой-нибудь fifo(7 ).

Вы можете использовать библиотеку JSONRPC. Или XDR (возможно, ONC/RPC/XDR) или ASN /1 для двоичной связи между разнородными компьютерами в центре обработки данных или MPI. Если вы можете использовать какой-нибудь суперкомпьютер, он, вероятно, имеет проприетарные библиотеки для облегчения обмена сообщениями между процессами, работающими на разные узлы.

Рассмотрите возможность использования OpenMPI.

Я пытаюсь связаться с внешней программой, которая, если она будет выполнена, запустит терминальный интерфейс.

Возможно, вам понадобится pty(7) с termios(3) ? Затем черпайте вдохновение из исходного кода xterm или rxvt.

Возможно, вам понадобится цикл событий вокруг poll(2) перед попыткой read(2) (или recv(2)...) или write(2) (или отправить(2))

Вы можете найти библиотеки с открытым исходным кодом (например, Glib, libev, ...) вам в помощь, и вам обязательно стоит изучить для вдохновения их исходный код.

person Basile Starynkevitch    schedule 03.02.2021
comment
Обязательно присмотрюсь к этим инструментам. Я хотел напрямую использовать каналы и дочерний процесс, так как думал, что это будет более эффективно. Это общение в конечном итоге приведет к тому, что несколько МБ инструкций и результатов будут анализироваться туда и обратно миллионы раз, поэтому эффективность является ключевым моментом. - person Snaporaz; 04.02.2021

Поскольку в общем случае я не знаю длину сообщения, которое мне нужно прочитать из формы fd_c2p, мне нужно создать внешний вид, который прослушивает канал до тех пор, пока он не станет пустым.

Для этого необходимо добавить O_NONBLOCK к дескриптору файла в родительском элементе, как это предложил @some-programmer-dude:

    // 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], он возвращает ошибку всякий раз, когда мы пытаемся прочитать из пустого файла. Чтение кода ошибки в 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] = '\0';
      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)] == '\0')
        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] = '\0';
      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)] == '\0')
        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);
}
person Snaporaz    schedule 04.02.2021