11

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?!

3
  • 2
    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). Jun 19, 2018 at 20:31
  • 1
    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, 2018 at 20:35
  • 1
    yes sorry. Edited now just before the 5 minutes deadline Jun 19, 2018 at 20:36

3 Answers 3

13

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
  • if echo were a program >f would be a redirection of stdout (fd 0) of the echo process, which would explain well that the file descriptor is closed when the process terminates. But if echo is a builtin, shouldn't >f be a redirection of the stdout of the bash process itself? How to understand then in this case that the file descriptor gets closed? Or is it that the redirection is reset and redirected back to the terminal when the process terminates, resulting in the writing end of f to being closed?
    – The Quark
    Nov 3, 2021 at 19:14
  • @TheQuark Redirection works the same for built-in utilities as it does for external utilities. When your run echo, you don't know whether it's built-in or external. The bash shell may be built without its echo built-in utility, or it may have been disabled earlier in the script with enable -n echo, or overridden by a shell function. It does not matter as redirections work the same for all those alternatives.
    – Kusalananda
    Nov 3, 2021 at 20:43
  • "Redirection works the same for built-in utilities as it does for external utilities." This I understand. My question is about how it is achieved: builtin run by a new bash child-process or rather temporary redirection of the file descriptor of the shell?
    – The Quark
    Nov 4, 2021 at 7:41
  • 1
    @TheQuark It works the same way for built-ins as for external utilities. The shell's standard output file descriptor is duplicated for use by the utility. The duplicated file descriptor is redirected to someplace else by the redirection on the command line. Unless required due to being part of a pipeline etc., bash does not spawn a sub-shell for the invocation of a built-in utility, and it does not do so for the invocation of an external utility either.
    – Kusalananda
    Nov 4, 2021 at 11:45
  • @TheQuark The main thing that is different is that bash does not do an exec() library call to actually run the built-in utility.
    – Kusalananda
    Nov 4, 2021 at 11:48
7

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.

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

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.

2
  • 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, 2018 at 5:31
  • To be honest, I'm not sure what I was trying to say in that comment, I'll just delete it. Sep 16, 2019 at 14:27

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.