As far as I understood one end of a pipe has both read and write fd's and the other end also has read and write fd's. Thats why when we are writing using fd[1], we are closing the read end e.g. fd[0] of the same side of the pipe and when we are reading from the 2nd end using fd[0] we close the fd[1] of that end. Am I correct?

So the shell creates the pipe, forks, and just before executing it remaps the pipe to the stdin or stdout of the child processes, making data flow from process one to process 2. As shell pipes are not bidirectional, they only use one end of the pipe and close the other end (it actually closes the other end too, after duplicating the filedescriptor to stdin or stdout).

This is a summary of the chat conversationDennis and I had, simplified so even other newbies can use it. When executing ls | less:

The bash shell is the parent process that has fd[0], fd[1], and fd[2].

pipe() is called by the parent, and it creates a pipe which is nothing but a buffer to accommodate data, and it has a file descriptor at each end, one for reading and one for writing.

The child process inherits all the open fds; 0, 1, and 2, as well as the two for the pipe. Thus ls has its own copy of the buffer with fd[0] and fd[1], but both of them point to the same pipe fds from bash. Inside the first child process, ls is executed and now we need to remap the output to the write end of the pipe (fd[1]) and then to the console. So close(1) and dup(fd[1]) will do the remapping. close(1) only affects the ls child process, not the bash parent. Now the output of ls will be directed to the fd[1] write end of the pipe to the child. fd[0] is still the read end, and we don't want the data read by ls itself, so we still need to close that end with close(fd[0]). Again, this only closes the file descriptor in ls, not bash.

We again fork to make the child process less, which inherits the pipe file descriptors. Input to less needs to read from the buffer, so we make use of fd[0]. Since fd[0] isn't stdin anymore, we need to remap it, so we close(0) and dup(fd[0]). Now fd[0] will point to stdin, so the input here is the output of ls. less uses fd[0] for reading and closes the write end fd[1] as less wants to prevent the data read by its write from being written out again