2
votes

I'm new to piping and have been trying to create a pair of pipes which allow a child process to write to the parent process, and the parent process to communicate back. There is 1 parent with up to 4 children. The child becomes a different program with exec.

What I have working:

Writing from the parent to the child process. When I read in the child program's stdin, it will receive what I wrote from the parent.

The aim:

To create a card game where the parent talks to each individual client (the child processes) and gives all the moves and information to them, from its stdout to the children's stdin. The individual child processes give back their moves on their stdout, read by the main parent. The moves that the game makes is fully decided by a sequence, not players. So it's a bot game.

What I am stuck with:

I'm not sure how to get it so the parent can read the child's stdout through a file stream. When I try to setup the reading from child lines, the code seems to stop working. Not even the child can read from parent (it seems to stop at the now commented out liens for setting up child to parent).

I also am unsure how to "wait" until something appears. Like, at the start the players have to send a "ready" message back to the parent to let them know they are working. Once I send the "ready" from the child, how do I "wait" indefinitely until the next message appears?

I'm not sure if I'm setting up the pipes correctly. Can someone provide guidance on how to use communication pipes and confirm my logic below?

What I gather for getting parent to write to child is:

  1. Create the pipe first
  2. Fork off the parent process into another process (child)
  3. Connect the pipe's in to the parent's stdout, and close off the reading side for the parent using dup2 and close
  4. Connect the pipe's out to the child's stdin, and close off the writing part for the child using dup2 and close
  5. Get a file stream using fdopen() from the file descriptor and then print to that.
  6. The child process stdin is now whatever you print to stdout from the parent.

Is this correct? I tried applying this kind of logic for child to parent but reversing it.

  1. Connect the in pipe to the read file stream, which is where the child program writes to from its stdout.
  2. Connect the out pipe to the read stream, where the parent reads from.

    void start_child_process(Game *game, int position) {
      int child2Parent[2];
      int parent2Child[2];
    
      if (pipe(parent2Child)) {
          printf("PIPE FAIL!\n");
      }
      if (pipe(child2Parent)) {
          printf("PIPE FAIL!\n");
      }
    
      pid_t pid = fork();
      game->readStream[position] = fdopen(child2Parent[0], "r");
      game->writeStream[position] = fdopen(parent2Child[1], "w");
    
      if (pid) { // Parent
          // Write from parent to child
          close(parent2Child[0]);
          dup2(fileno(game->writeStream[position]), STDOUT_FILENO);
          fprintf(game->writeStream[position], "%s", "test message");
          fflush(game->writeStream[position]);
          close(parent2Child[1]);
    
          // Read from child -- not working
    
          /*dup2(child2Parent[0], STDIN_FILENO);
          close(child2Parent[0]);
          close(child2Parent[1]);
          */
    
      } else {
          // Setup child to read from stdin from parent
          dup2(parent2Child[0], STDIN_FILENO);
          close(parent2Child[1]);
    
          // Setup writing from child to parent
    
          /*
          if (dup2(child2Parent[1], STDOUT_FILENO) == -1) {
              fprintf(stderr, "dup2 in child failed\n");
          } else {
              fprintf(stderr, "dup2 in child successful\n");
              close(child2Parent[0]);
              close(child2Parent[1]);
          }
          */
    
    
          if ((int)execl("child", "2", "A", NULL) == -1) {
              printf("Failed child process\n");
          }
    
      }
     }
    

My child main has the following which reads it:

char string[100];
printf("reading from pipe: %s\n", fgets(string, 100, stdin));

But I'm not sure how

Also, I'm not permitted to use popen() or write(). I'm also encouraged to use file streams apparently.

1

1 Answers

1
votes

I speak principally to your main question of establishing two-way communication between parent and child processes. If you would like additional answers then please pose separate questions.

You seem to have a reasonable general approach, but you do have one serious misconception / design flaw: whereas it is reasonable for multiple clients to each connect their standard streams to pipes for communicating with the parent process, you cannot connect the parent's end of all those pipes to the parent's standard streams if you want to be able to handle more than one client at a time. The parent only has one set of standard streams, after all. To support multiple clients, then, the parent process must maintain a separate pair of file descriptors and/or streams for each one, and must communicate via those instead of via its standard streams.

I am uncertain why your parent / child communication is failing when you hook up the child-to-parent direction. The process is indeed analogous to setting up the other other endpoint. Here is a working example:


parent.c:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>

int main() {
  int child2Parent[2];
  int parent2Child[2];
  char buffer[256];
  FILE *p2cStream;
  FILE *c2pStream;
  pid_t pid;

  if (pipe(parent2Child) || pipe(child2Parent)) {
      perror("Failed to create pipes");
      exit(EXIT_FAILURE);
  }

  switch (pid = fork()) {
    case -1: /* error */
      perror("Failed to fork");
      break;
    case 0:  /* child */
      // Setup child to read from stdin from parent
      close(parent2Child[1]);  /* ignoring any error */
      close(child2Parent[0]);  /* ignoring any error */
      if ((dup2(parent2Child[0], STDIN_FILENO) < 0)
          || (dup2(child2Parent[1], STDOUT_FILENO) < 0)) {
        perror("Failed to duplicate file descriptors");
      } else {
        /* conventionally, the first program argument is the program name */
        /* also, execl() returns only if it fails */
        execl("child", "child", "2", "A", NULL);
        perror("Failed to exec child process");
      }

      exit(EXIT_FAILURE);
      break;
    default: /* parent */
      close(parent2Child[0]);  /* ignoring any error */
      close(child2Parent[1]);  /* ignoring any error */
      if (!(p2cStream = fdopen(parent2Child[1], "w"))
          || !(c2pStream = fdopen(child2Parent[0], "r"))) {
        perror("Failed to open streams");
        exit(EXIT_FAILURE);
      }
      if ((fprintf(p2cStream, "test message from parent\n") < 0) 
          || fclose(p2cStream)) {
        perror("Failed to write to the child");
        exit(EXIT_FAILURE);
      }
      if (fscanf(c2pStream, "%255[^\n]", buffer) < 1) {
        perror("Failed to read the child's message");
        exit(EXIT_FAILURE);
      }
      printf("The child responds: '%s'\n", buffer); /* ignoring any error */
      break;
  }

  return EXIT_SUCCESS;
}

child.c:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    char buffer[256] = { 0 };

    if (scanf("%255[^\n]", buffer) < 0) {
        perror("Failed to reading input");
        exit(EXIT_FAILURE);
    }

    /*
     * If stdout is connected to the parent then we must avoid
     * writing anything unexpected to it
     */
    if (fprintf(stderr, "received: '%s'\n", buffer) < 0) {
        perror("Failed to echo input");
        exit(EXIT_FAILURE);
    }

    printf("Hi, Mom!\n");  /* ignoring any error */
    fflush(stdout);        /* ignoring any error */

    return EXIT_SUCCESS;
}

In contrast to your code, do note

  • the attention to checking all return values that may signal an error that I care about;
  • the parent's use of streams other than the standard streams for communicating with the child (though with only one child, that's a convenience rather than a necessity);
  • the convention for execl() arguments.

Note also that as for waiting for something to "appear", I/O operations on the IPC streams you set up this way will automatically produce that effect. How you can or should make use of that, however, is certainly a different issue.