10

Two windows, same user, with bash prompts. In window-1 type:

$ mkfifo f; exec <f

So bash is now attempting to read from file descriptor 0, which is mapped to named pipe f. In window-2 type:

$ echo ls > f

Now window-1 prints an ls and then the shell dies. Why?

Next experiment: open window-1 again with exec <f. In window-2 type:

$ exec 3>f
$ echo ls >&3

After the first line above, window-1 wakes up and prints a prompt. Why? After the second line above, window-1 prints the ls output and the shell stays alive. Why? In fact, now in window-2, echo ls > f does not close the window-1 shell.

The answer must have to do with the existence of the file descriptor 3 from window-2 referencing the named pipe?!

  • 1
    After exec <f, bash is not attempting to read from f, it is first attempting to open it. The open() won't return until there is some process doing another open in write mode to the pipe (at which point the pipe will be instantiated, and the shell will read input from it). – Stéphane Chazelas Jun 19 '18 at 20:31
  • Excellent point, @StéphaneChazelas. This must be why, once exec 3>f is run, the first shell then gives a prompt. (Minor point, did you mean "in write mode" in your comment?) – Fixee Jun 19 '18 at 20:35
  • yes sorry. Edited now just before the 5 minutes deadline – Stéphane Chazelas Jun 19 '18 at 20:36
12

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.

6

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.

  • Seems kind of analogous to reference counts in Java (and other OO languages)! Makes sense though. – Fixee Jun 19 '18 at 20:14
2

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.

  • 1
    Here it's special in that echo is built-in in most shells, but for normal non-builtin commands, in cmd > f, the shell would fork a process in which it would open f for writing on fd 1 and then execute cmd in that same process. When that process exits, there's no more process with a fd open in write mode to the pipe, so the processes with a fd open in read mode to the pipe will see EOF – Stéphane Chazelas Jun 19 '18 at 20:33
  • This is a good example of why you should leave the first three file descriptors alone unless there is good design reason to modify them. When you used the descriptor (1) that ended up being the input descriptor (0) to the other shell, you not only closed the pipe (and what you were doing with that particular data stream), but also closed the input to the second shell which caused it to terminate. This is fine, but only if you're doing it on purpose. Using higher numbered file descriptors avoids side effects like this because nothing expects them to be in any particular state or even defined. – Joe Jun 23 '18 at 5:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.