5
votes

An assignment in my Operating Systems class requires me to build a binary process tree by recursively calling exec on the same program. The goal is to split some arbitrary task into separate processes. The parent should communicate with the children, and the children with the parent only via unnamed pipes. The idea is that the parent sends each child half of the work and this continues recursively until a base case is met where the length of the string being passed to each child is <= 2. The child then processes this data and sends the results back to the parent via pipes.

To get a better understanding of how two way communication works with pipes in c I created the following simple program before moving on to the actual assignment. The parent never reads the data from the child process though. I'm expecting the output...

in parent | message received: test

Instead, when I print I get...

in parent | message received:

It seems that buff is empty and not reading from the child process. Can someone please explain what I'm doing wrong and/or the standard way of

  1. writing to exec'd child from parent
  2. reading from parent in exec'd child
  3. writing back to parent from exec'd child
  4. reading from exec'd child in parent

I am required to use exec(), pipe(), fork(). Thank you.

/**
 * *********************************
 * two_way_pipes.c
 * *********************************
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <math.h>
#include <sys/time.h> 
#include <sys/types.h> 
#include <unistd.h>

#define PARENT_READ read_pipe[0]
#define PARENT_WRITE write_pipe[1]
#define CHILD_WRITE read_pipe[1]
#define CHILD_READ  write_pipe[0]

#define DEBUGGING 1

int main(int argc, char **argv) {
    char buff[5];

    // in the child process that was exec'd on the orginal call to two_way_pipes
    if(argc == 2) {
        read(STDIN_FILENO, buff, 4); // this should read "test" from stdin
        buff[4] = '\0';
        fprintf(stdout, "%s\n", buff); // this should right "test" to stdout and be read by the parent process
    // int the root process, the original call to two_way_pipes with no args
    } else {
        int pid;
        int read_pipe[2];
        int write_pipe[2];

        pipe(read_pipe);
        pipe(write_pipe);

        pid = fork();

        // parent process
        if(pid > 0) {
            close(CHILD_READ);
            close(CHILD_WRITE);

            write(PARENT_WRITE, "test", 4); // attempting to write this to the child

            struct timeval tv;
            fd_set readfds;
            tv.tv_sec = 10;
            tv.tv_usec = 0;
            FD_ZERO(&readfds);
            FD_SET(PARENT_READ, &readfds);
            select(PARENT_READ + 1, &readfds, NULL, NULL, &tv);

            if(FD_ISSET(PARENT_READ, &readfds)) {
                read(PARENT_READ, buff, 4); // should read "test" which was written by the child to stdout
                buff[4] = '\0';
                close(PARENT_READ);
                close(PARENT_WRITE);
                fprintf(stderr, "in parent | message received: %s\n", buff);  // "test" is not in buff
            }

        // child process
        } else if(pid == 0) {
            close(PARENT_READ);
            close(PARENT_WRITE);

            dup2(CHILD_READ, STDIN_FILENO);
            dup2(CHILD_WRITE, STDOUT_FILENO);
            close(CHILD_READ);
            close(CHILD_WRITE);

            char *argv2[] = {"some random arg to make sure that argc == 2 in the child", NULL};
            execvp("two_way_pipes", argv2);
            _exit(0);
        // error forking child process
        } else {
            fprintf(stderr, "error forking the child\n");
        }
    }
}

Update

Based on Jonathon's answer I modified the arg2 array being passed into execvp to...

char *argv2[] = {"two_way_pipes", "1", NULL};
execvp("two_way_pipes", argv2);

This didn't fix the issue. The parent still wasn't able to read "test" back from the client. However, in response to Jonathon's answer and William's comment I started tweaking my exec call and for some reason changing it to the line show below worked.

execl("two_way_pipes", "two_way_pipes", "1", NULL);

I'll gladly accept any answers explaining why the execvp call wouldn't work but the execl call did.

3
Is your exevp successful? Try replacing the _exit(0) with perror( "execvp" )William Pursell
Thanks William. I replaced the _exit(0) call with perror("execvp"). That error message never printed but I started tweaking my exec call and for some reason changing the call to execl("two_way_pipes", "two_way_pipes", "1", NULL); fixed the problem.Anthony Jack
This code consequently misses to test the outcome of system calls. Not doing so makes testing and debugging hard and the program weak. Please see my answer.alk

3 Answers

6
votes

Besides the issue mentioned by Jonathon Reinhart, most probably the call to execv() fails.

To test this modify these lines

execvp("two_way_pipes", argv2);
_exit(0);

to be

...
#include <errno.h>
...


execvp("two_way_pipes", argv2); /* On sucess exec*() functions never return. */
perror("execvp() failed); /* Getting here means execvp() failed. */
_exit(errno);

Expect to receive

execvp() failed: No such file or directory

To fix this change

execvp("two_way_pipes", argv2);

to be

execvp("./two_way_pipes", argv2);

Also if the child was not exec*()ed then this line

read(PARENT_READ, buff, 4); // should read "test" which was written by the child to stdout

fails and in turn buff is not initialised and therefore this line

fprintf(stderr, "in parent | message received: %s\n", buff);  

provokes undefined behaviour.

To fix this at least properly initialise buff by changing

char buff[5];

to be

char buff[5] = "";
2
votes

The first entry in your argv2 should be the name of the executable (just like your incoming argv[0].

char *argv2[] = {"two_way_pipes", "some random arg...", NULL};
execvp("two_way_pipes", argv2);

From the execvp man page:

The execv(), execvp(), and execvpe() functions provide an array of pointers to null-terminated strings that represent the argument list available to the new program. The first argument, by convention, should point to the filename associated with the file being executed. The array of pointers must be terminated by a NULL pointer.

1
votes

As Jonathon Reinhart says you should change this lines:

char *argv2[] = {"some random arg to make sure that argc == 2 in the child", NULL};
execvp("two_way_pipes", argv2);

to:

char *argv2[] = {argv[0], "some random arg...", NULL};
execvp(argv[0], argv2);

then it works as expected:

echo test | ./two_way_pipes
in parent | message received: test

In your program you wrote "two_way_pipes" but it is not in your PATH so you really need the extra ./ so argv[0] then is ("./two_way_pipes").