0

I am confused as to how to properly use close to close pipes in C. I am fairly new to C so I apologize if this is too elementary but I cannot find any explanations elsewhere.

#include <stdio.h>

int main()
{
    int fd[2];
    pipe(fd);
    if(fork() == 0) {
        close(0);
        dup(fd[0]);
        close(fd[0]);
        close(fd[1]);
    } else {
        close(fd[0]);
        write(fd[1], "hi", 2);
        close(fd[1]);
    }
    wait((int *) 0);
    exit(0);

}

My first question is: In the above code, the child process will close the write side of fd. If we first reach close(fd[1]), then the parent process reach write(fd[1], "hi", 2), wouldn't fd[1] already been closed?

int main()
{
    char *receive;
    int[] fd;
    pipe(fd);
    if(fork() == 0) {
       while(read(fd[0], receive, 2) != 0){
            printf("got u!\n");
       }
    } else {
        for(int i = 0; i < 2; i++){
            write(fd[1], 'hi', 2);
        }
        close(fd[1]);
    }
    wait((int *) 0);
    exit(0);

}

The second question is: In the above code, would it be possible for us to reach close(fd[1]) in the parent process before the child process finish receiving all the contents? If yes, then what is the correct way to communicate between parent and child. My understanding here is that if we do not close fd[1] in the parent, then read will keep being blocked, and the program won't exit either.

swag2198
  • 2,546
  • 1
  • 7
  • 18
penny
  • 215
  • 1
  • 9
  • 2
    A file isn't really closed until all processes that have a reference to it close their descriptors. So it doesn't matter what order the closes happen, because the other process keeps it open. – Barmar Nov 16 '20 at 23:58
  • 2
    The child process can close its file descriptors, but that does not dissociate the parent's file descriptors from the underlying open file description in the kernel. And *vice versa*. – John Bollinger Nov 17 '20 at 00:08
  • Your child process doesn’t read from the pipe, but it could. Your parent process does write to the child. The closes are correct under all normal circumstances. In abnormal circumstances, using `dup2()` could be safer, but we're talking about very abnormal circumstances. – Jonathan Leffler Nov 17 '20 at 00:21
  • In your second program, you have an uninitialized pointer. Reading into that will cause unhappiness. – Jonathan Leffler Nov 17 '20 at 00:23

2 Answers2

3

First of all note that, after fork(), the file descriptors fd would also get copied over to the child process. So basically, a pipe acts like a file with each process having its own references to the read and write end of the pipe. Essentially there are 2 read and 2 write file descriptors, one for each process.

My first question is: In the above code, the child process will close the write side of fd. If we first reach close(fd[1]), then the parent process reach write(fd[1], "hi", 2), wouldn't fd[1] already been closed?

Answer: No. The fd[1] in parent process is the parent's write end. The child has forsaken its right to write on the pipe by closing its fd[1], which does not stop the parent from writing into it.

Before answering the second question, I fixed your code to actually run it and produce some results.

int main()
{
    char receive[10];
    int fd[2];
    pipe(fd);
    if(fork() == 0) {
        close(fd[1]);      <-- Close UNUSED write end
       while(read(fd[0], receive, 2) != 0){
            printf("got u!\n");
            receive[2] = '\0';
            printf("%s\n", receive);
       }
       close(fd[0]);       <-- Close read end after reading
    } else {
        close(fd[0]);      <-- Close UNUSED read end
        for(int i = 0; i < 2; i++){
            write(fd[1], "hi", 2);
        }
        close(fd[1]);      <-- Close write end after writing

        wait((int *) 0);
    }
    exit(0);

}

Result:

got u!
hi
got u!
hi

Note: We (seemingly) lost one hi because we are reading it into same array receive which essentially overrides the first hi. You can use 2D char arrays to retain both the messages.

The second question is: In the above code, would it be possible for us to reach close(fd[1]) in the parent process before the child process finish receiving all the contents?

Answer: Yes. Writing to a pipe() is non-blocking (unless otherwise specified) until the pipe buffer is full.

If yes, then what is the correct way to communicate between parent and child. My understanding here is that if we do not close fd[1] in the parent, then read will keep being blocked, and the program won't exit either.

If we close fd[1] in parent, it will signal that parent has closed its write end. However, if the child did not close its fd[1] earlier, it will block on read() as the pipe will not send EOF until all the write ends are closed. So the child will be left expecting itself to write to the pipe, while reading from it simultaneously!

Now what happens if the parent does not close its unused read end? If the file had only one read descriptor (say the one with the child), then once the child closes it, the parent will receive some signal or error while trying to write further to the pipe as there are no readers.

However in this situation, parent also has a read descriptor open and it will be able to write to the buffer until it gets filled, which may cause problems to the next write call, if any.

This probably won't make much sense now, but if you write a program where you need to pass values through pipe again and again, then not closing unused ends will fetch you frustrating bugs often.

swag2198
  • 2,546
  • 1
  • 7
  • 18
  • Thank you very much, and I learned a ton! But given your new version of the code, if the parent process closes fd[1] first, wouldn't it be possible for the child process to fail to read all the content being sent? I guess my question is: would "read" stop reading immediately after both pipes are closed or would it keep reading all the contents already in the pipe before the pipe is closed? – penny Nov 17 '20 at 01:30
  • Read from a pipe will not stop until the reader's buffer is full (read everything from pipe) or `EOF` is received. Parent closing its write end won't stop the child from reading whatever is present in the pipe already. However parent not closing its write end, will block the child in `read` call, as it will not receive `EOF` and keep on expecting content in pipe. – swag2198 Nov 17 '20 at 01:33
  • Also note that `EOF` is sent through pipe to the readers, only when all of its available write ends are closed. So that is also a reason why you should close the unused write end in child first. Feel free to ask any further clarifications. – swag2198 Nov 17 '20 at 01:37
1

what is the correct way to communicate between parent and child[?]

The parent creates the pipe before forking. After the the fork, parent and child each close the pipe end they are not using (pipes should be considered unidirectional; create two if you want bidirectional communication). The processes each have their own copy of each pipe-end file descriptor, so these closures do not affect the other process's ability to use the pipe. Each process then uses the end it holds open appropriately for its directionality -- writing to the write end or reading from the read end.

When the writer finishes writing everything it intends ever to write to the pipe, it closes its end. This is important, and sometimes essential, because the reader will not perceive end-of-file on the read end of the pipe as long as any process has the write end open. This is also one reason why it is important for each process to close the end it is not using, because if the reader also has the write end open then it can block indefinitely trying to read from the pipe, regardless of what any other process does.

Of course, the reader should also close the read end when it is done with it (or terminate, letting the system handle that). Failing to do so constitutes excess resource consumption, but whether that is a serious problem depends on the circumstances.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157