Pipes are first-in first-out. Your problem is that you misunderstand when the “in” happens. The “in” event is writing, not opening.
Removing useless punctuation, your code is:
echo a > fifo & echo b > fifo &
This runs the commands echo a > fifo
and echo b > fifo
in parallel. Whatever gets in first will get out first, but it's a roughly equal race as to which one gets in first.
If you want a
to be read before b
, you have to arrange to write it before b
. This means you have to wait until echo a > fifo
is done before you start echo b > fifo
.
{ echo a > fifo; echo b > fifo; } & cat fifo
If you want to dig further, you need to distinguish between the elementary operations that happen under the hood. echo a > fifo
combines three operations:
- Open
fifo
for writing.
- Write two characters (
a
and a newline) to the file.
- Close the file.
You can arrange for these operations to happen at different times:
(
exec >fifo # 1. open
sleep 1
echo a # 2. write
sleep 1
) # 3. close
Likewise, cat foo
combines an open, a read and a close operation. You can separate them:
(
exec <fifo # 1. open
sleep 1
read line # 2. read
echo $line
sleep 1
) # 3. close
(The read
shell builtin might actually make multiple read
system calls, but that's not important right now.)
Fifos are actually not quite pipes. They're more like potential pipes. The fifo is a directory entry, and a pipe object is created when a process opens the fifo for reading. If a process opens the fifo for writing while no pipe exists, the open
call blocks until a pipe is created. Furthermore, if a process opens the fifo for reading, this operation also blocks until a process opens the fifo for writing (unless the reader opens the pipe in non-blocking mode, which is not convenient from the shell). As a consequence, the first open-for-reading and the first open-for-writing on a named pipe will return at the same time.
Here's a shell script that puts this knowledge in action.
#!/bin/sh
tick () { sleep 0.1; echo tick; echo 0.1; }
mkfifo fifo
{
exec <fifo; echo >&2 opened for reading;
echo a; echo >&2 wrote a
} & writer=$!
tick
{
exec >fifo; echo >&2 opened for writing;
exec cat >&2;
} & reader=$!
wait $writer
kill $reader
rm fifo
Note how both openings happen at the same time (as near as we can observe). And the write can only happen after that.
Note: there is actually a race condition in the script above — but it's not related to the pipes. The echo >&2
commands are racing against cat >&2
to write to the terminal, and so you might see a
from cat
before opening for writing
and wrote a
. If you want to have a more precise view of the timing, you can trace the system calls. For example, under Linux:
mkfifo fifo
strace -f -P fifo sh -c '…'
Now if you put two writers, both writers will block at the opening step until the reader arrives. It doesn't matter who starts the open
call first: pipes are first-in-first-out for data, not for open attempts. Whoever writes first is what matters. Here's a script to experiment with this.
#!/bin/sh
mkfifo fifo
{
exec >fifo; echo >&2 opened for writing a
sleep $1
echo a; echo >&2 wrote a
} & writer_a=$!
{
exec >fifo; echo >&2 opened for writing b
sleep $2
echo b; echo >&2 wrote b
} & writer_b=$!
sleep 0.2
cat fifo & reader=$!
wait $writer_a
wait $writer_b
kill $reader
rm fifo
Call the script with two arguments: the wait time for reader a and the wait time for writer b. The reader comes online after 0.2 seconds. If both wait times are less than 0.2 seconds, both writers will try to write as soon as the writers come online, and it's a race. On the other hand, if the wait times are more than 0.2, whoever comes first gets output first.
$ ./c 0.1 0.1
# Order of "opened for writing": random
# Order of "a"/"b": random
# Order of "wrote": random, might not match a/b due to echo racing against each other
$ ./c 0.3 0.4
# Order of "opened for writing": random
# Order of "wrote": a then b
# Order of "a"/"b": a then b
sleep
delayscat
as well asecho
. I’m only delayingecho
.a\nb\n
andb\na\n
are the two results you expect to see, butab\n\n
andba\n\n
may also possible, if one writer interrupts the other mid-write. You need to synchronize writes to the same pipe (which in shell means using something likeflock
).