Search code examples
cpipeforkblocking

Why does read() block and wait forever in parent process despite the writing end of pipe being closed?


I'm writing a program with two processes that communicate through a pipe. The child process reads some parameters from the parent, executes a shell script with them and returns the results to the parent process line by line.

My code worked just fine until I wrote the while(read()) part at the end of the parent process. The child would execute the shell script, read its echo's from popen() and print them to standard output.

Now I tried to write the results to the pipe as well and read them in the while() loop at the parent's end but it blocks and neither will the child process print the result to standard output. Apparently it won't even reach the point after reading the data from the pipe sent by the parent.

If I comment out the while() at the parent process, the child will print the results and return, and the program will end smoothly.

Why does the while(read()) block even if I closed the writing end of the pipe in both parent and child processes?

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

int read_from_file(char **directory, int *octal) {
        FILE *file = fopen("input", "r");
        if (file == NULL) {
                perror("error opening file");
                exit(1);
        }
        fscanf(file, "%s %d", *directory, octal);
}

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

        char *directory = malloc(256);
        int *octal = malloc(sizeof *octal);

        pid_t pid;
        int pfd[2];
        char res[256];
        if (pipe(pfd) < 0) {
                perror("Error opening pipe");
                return 1;
        }

        if ((pid = fork()) < 0)
                perror("Error forking");

        if (pid == 0) {
                printf("client here\n");
                if (read(pfd[0], directory, 256) < 0)
                        perror("error reading from pipe");
                if (read(pfd[0], octal, sizeof(int)) < 0)
                        perror("error reading from pipe");
// This won't get printed:
                printf("client just read from pipe\n");
//              close(pfd[0]);

                char command[256] = "./asd.sh ";
                strcat(command, directory);
                char octal_c[5];
                sprintf(octal_c, " %d", *octal);
                strcat(command, octal_c);

                FILE *f = popen(command, "r");
                while (fgets(res, 256, f) != NULL) {
                        printf("%s", res);
                        if (write(pfd[1], res, 256) < 0)
                                perror("Error writing res to pipe");
                }
                fclose(f);
                close(pfd[1]);
                close(pfd[0]);
                fflush(stdout);
                return 1;
        }

        read_from_file(&directory, octal);

        if (write(pfd[1], directory, 256) < 0)
                perror("Error writing dir to pipe");
        if (write(pfd[1], octal, sizeof(int)) < 0)
                perror("error writing octal to pipe");

        int r;
        close(pfd[1]);

        while (r = read(pfd[0], res, 256)) {
                if (r > 0) {
                        printf("%s", res);
                }
        }
        close(pfd[0]);

        while (wait(NULL) != -1 || errno != ECHILD);
}

Solution

  • Since the child demonstrably reaches ...

                    printf("client here\n");
    

    ... but seems not to reach ...

                    printf("client just read from pipe\n");
    

    ... we can suppose that it blocks indefinitely on one of the two read() calls between. With the right timing, that explains why the parent blocks on its own read() from the pipe. But how and why does that blocking occur?

    There are at least three significant semantic errors in your program:

    1. pipes do not work well for bidirectional communication. It is possible, for example, for a process to read back the bytes that it wrote itself and intended for a different process. If you want bidirectional communication then use two pipes. In your case, I think that would have avoided the apparent deadlock, though it would not, by itself, have made the program work correctly.

    2. write and read do not necessarily transfer the full number of bytes requested, and short reads and writes are not considered erroneous. On success, these functions return the number of bytes transferred, and if you want to be sure to transfer a specific number of bytes then you need to run the read or write in a loop, using the return values to track progress through the buffer being transferred. Or use fread() and fwrite() instead.

    3. Pipes convey undifferentiated streams of bytes. That is, they are not message oriented. It is not safe to assume that reads from a pipe will be paired with writes to the pipe, so that each read receives exactly the bytes written by one write. Yet your code depends on that to happen.

    Here's a plausible failure scenario that could explain your observations:

    The parent:

    1. fork()s the child.
    2. after some time performs two writes to the pipe, one from variable directory and the other from variable octal. At least the first of those is a short write.
    3. closes its copy of the write end of the pipe.
    4. blocks attempting to read from the pipe.

    The child:

    1. reads all the bytes written via its first read (into its copy of directory).
    2. blocks on its second read(). It can do this despite the parent closing its copy of the write end, because the write end of the pipe is still open in the child.

    You then have a deadlock. Both ends of the pipe are open in at least one process, the pipe is empty, and both processes are blocked trying to read bytes that can never arrive.

    There are other possibilities that arrive at substantially the same place, too, some of them not relying on a short write.