Reading from two fifos in Bash
I’m trying to read from two fifos (read from one, if there’s no content read from the other, and if there’s no content in neither of both try it again later), but it keeps blocking the process (even with the timeout option).
I’ve followed some other questions that reads from files (How to read from two input files using while loop, and Reading two files into an IFS while loop) and right now I have this code:
while true; do
while
IFS= read -r msg; statusA=$?
IFS= read -u 3 -r socket; statusB=$?
[ $statusA -eq 0 ] || [ $statusB -eq 0 ]; do
if [ ! -z "$msg" ]; then
echo "> $msg"
fi
if [ ! -z "$socket" ]; then
echo ">> $socket"
fi
done <"$msg_fifo" 3<"$socket_fifo"
done
Is there something I’m doing wrong? Also, I can’t use paste
/cat
piped or it blocks the process completely.
This is by default and by design, and I am not sure you can solve this with bash
. You can certainly solve this with zsh
(it has select() syscall module) but maybe shell is not the right language for the job at this point.
The issue is simple as everything in unix is, but actual understanding and solution requires some deeper thinking and practical knowledge.
Cause of the effect is blocking flag set on the file descriptor, which is done by the system kernel by default when you open any VFS object.
In unix, process cooperation is synchronized by blocking at IO boundaries, which is very intuitive and makes everything very elegant and simple and makes naive and simplistic programming of beginner and intermediate application programmers "just work".
When you open object in question (file, fifo or whatever), the blocking flag set on it ensures, that any read form the descriptor immediately blocks the whole process when there is no data to be read. This block is unblocked only and only after some data is filled into the object from the other side (in case of pipe).
Regular files are an "exception", at least when compared to pipe, as from the point of IO subsystem they "never block" (even if they actually do – ie unsetting blocking flag on file fd has no effect, as process block happens deeper in kernel’s storage fd read routine). From process POV disk read is always instant and in zero time without blocking (even though system clocks actually jump forward during such read).
This design has two effects you are observing:
First, you never really observe effects of IO blocking when juggling just regular files from shell (as they never block for real, as we explained above).
Second, once you get blocked in shell on read() from pipe, like you’ve got here, your process is essentially stuck in block forever as this kind of blocking is "for real", at least until more data is not filled in from the other side of the pipe. Your process does not even run and consume CPU time in that state, it is kernel who holds it blocked from outside, until more data arrives, and thus process timeout routines cannot even run either (as that requires process to be consuming CPU time ie running).
Your process will remain blocked at least until you fill pipe with sufficient amount of data to be read, then it will ublocked briefly, until all the data is consumed again and process is blocked again.
If you ponder about it carefully this is actually what makes pipes in shell work.
Have you ever wondered how come that complex shell pipeline adapts to fast or slow programs somehow on it’s own? This is the mechanism that makes it work. Fast generators spewing output fast will make next program in pipeline read it faster, and slow data generator in pipeline will make any subsequent program in pipeline read/run slower – everything is rate limited by the pipes blocking on data, synchonizing whole pipe as if by magic.
EDIT: further clarification
How to get out of it?
There is no easy way. Not in bash as far as I know.
Easiest one is to ponder more about the problem and redesign it different way.
Due nature of blocking explained above, the most simple is to understand imposed design constraint for shell programs: only have one main input stream.
This will make shell program robust enough to deal with both file input (not a problem) and pipe.
Reading from multiple pipes (ie. even two) will make your program block naturally until both of them have data, so if you can ensure that both pipes are full of data at all times this will work. Unfortunately this rarely works: the moment reading from pipes becomes intertwined and interleaved you have problem with pipes filling in random order – especially if reads are dependent first pipe to become empty will stall you whole processing. We call such situation deadlock.
You can solve problem of reading from multiple pipes by removing the blocking flag from file descriptors in question, but now you have IO scheduling and data multiplexing problem, which require properly equipped language to deal with.
I am afraid bash is not equipped well enough for that and even if it is you now need to learn more how this stuff works then.
I’ve seen the conversation between @etosan and @ilkkachu and I’ve tested your proposal of using exec fd<>fifo
and it works now. I’m not sure if that involves some kind of problem (as @etosan have said) but at least it works now.
exec 3<>"$socket_fifo" # to not block the read
while true; do
while
IFS= read -t 0.1 -r msg; statusA=$?
IFS= read -t 0.1 -u 3 -r socket; statusB=$?
[ $statusA -eq 0 ] || [ $statusB -eq 0 ]; do
if [ ! -z "$msg" ]; then
echo "> $msg"
fi
if [ ! -z "$socket" ]; then
echo ">> $socket"
fi
done <"$msg_fifo"
done
I’ll also consider your warnings about using Bash in this case.