Named pipes, file descriptors and EOF

It has to do with the closing of the file descriptor.

In your first example, echo writes to its standard output stream which the shell opens to connect it with f, and when it terminates, its descriptor is closed (by the shell). On the receiving end, the shell, which reads input from its standard input stream (connected to f) reads ls, runs ls and then terminates due to the end-of-file condition on its standard input.

The end-of-file condition occurs because all writers to the named pipe (only one in this example) have closed their end of the pipe.

In your second example, exec 3>f opens file descriptor 3 for writing to f, then echo writes ls to it. It's the shell that now has the file descriptor opened, not the echo command. The descriptor remains open until you do exec 3>&-. On the receiving end, the shell, which reads input from its standard input stream (connected to f) reads ls, runs ls and then waits for more input (since the stream is still open).

The stream remains open because all writers to it (the shell, via exec 3>f, and echo) have not closed their end of the pipe (exec 3>f is still in effect).


I have written about echo above as if it was an external command. It's most likely is built into the shell. The effect is the same nonetheless.


There's not much to it: when there are no writers to the pipe, it looks closed to readers, i.e. returns EOF when read and blocks when opened.

From the Linux man page (pipe(7), but see also fifo(7)):

If all file descriptors referring to the write end of a pipe have been closed, then an attempt to read(2) from the pipe will see end- of-file (read(2) will return 0).

Closing the write end is what implicitly happens at the end of the the echo ls >f, and as you say, in the other case, the file descriptor is kept open.


After reading the two answers from @Kusalananda and @ikkachu, I think I understand. In window-1, the shell is waiting for something to both open the write end of the pipe and then close it. Once the write end is opened, the shell in window-1 prints a prompt. Once the write end is closed, the shell gets EOF and dies.

On the window-2 side we have the two situations described in my question: in the first situation with echo ls > f, there is no file descriptor 3, so we have echo spawning, and its stdin and stdout look like this:

0 --> tty
1 --> f

Then echo terminates and the shell closes both descriptors. Since file descriptor 1 is closed and references f, the write end of f is closed and that causes an EOF to window-1.

In the second situation, we run exec 3>f in our shell, causing the shell to take this environment:

bash:
0 --> tty
1 --> tty
2 --> tty
3 --> f

Now we run echo ls >& 3 and the shell allocates file descriptors for echo as follows:

echo:
0 --> tty
1 --> f     # because 3 points to f
2 --> tty

Then the shell closes the three descriptors above, including f, but f still has a reference to it from the shell itself. This is the important difference. Closing descriptor 3 with exec 3>&- would close the last open reference and cause an EOF to window-1, as @Kusalananda noted.