For bash experts (pipe buffering)

Take the following example:

#!/bin/bash

ping -i 0.1 1.1.1.1 | while read ping_line
do
        echo $ping_line
        sleep 1
done

The processing on the right hand side of the pipe is slower than the rate that ping results come in. So the output will lag behind.

How do I skip lines such that I only echo the latest available ping?

This works:

#!/bin/bash

file=/tmp/test

ping -i 0.1 1.1.1.1 > $file&
sleep 1

tail -f $file | while read ping_line
do
        echo $ping_line
        sleep 0.2
        > $file
done

But it gives inexplicable high CPU usage in some systems. It starts off with low CPU, but then this creeps up with time. See here:

The actual context:

1 Like

Can you just tail -1 $file to always read the last line (using your second example of writing ping output to a file)?

You’d need a different loop structure to tail -1 the file every x seconds.

1 Like

Fun question but took me a while as I'm not a "bash expert"

if mkfifo and awk are options:

#!/usr/bin/sh
#
# also works for /usr/bin/bash
# For use on openwrt with fractional interval seconds:
#opkg update
#opkg install coreutils-date
#opkg install iputils-ping # i think? or compile busy box with ping enabled for fractional seconds

calc() {
    /usr/bin/awk "BEGIN{printf \"%.4f\n\", $*}"
}
lessThan() {
   /usr/bin/awk -v n1="$1" -v n2="$2" 'BEGIN { if(n1<n2) print 1; else print 0}'
}
ping_interval=0.2
skip_interval=1 # seconds
ping_output=/tmp/ping_pipe
/usr/bin/mkfifo $ping_output
/usr/bin/ping -i $ping_interval 1.1.1.1 >"$ping_output" &
currentTime=$(/usr/bin/date +%s.%N)
endTime=$(calc $currentTime + $skip_interval)
while read <"$ping_output" -r line; do
    if [ $(lessThan $(/usr/bin/date +%s.%N) $endTime) -eq 1 ]; then
        : # or do other stuff here until you need a ping
    else
        echo $line
        currentTime=$(/usr/bin/date +%s.%N)
        endTime=$(calc $currentTime + $skip_interval)
    fi
done

Don't forget rm /tmp/ping_output after running the test script above. Works for me, not sure about slow downs over time.

ref for while loop interval
ref to keep a named pipe open
ref for using awk to compare two non-integers
ref for doing non-integer math with awk. There are also prior www answers regarding formatting float output with awk - I used printf and %.4f\n for debugging purposes.

HTH

@dave14305 yes but then that file will just grow in perpetuity. I want to read as fast as possible from ping and skip if necessary. So I want a last in first out buffer that is flushed on every read.

@nmrh something like that looks promising.

Since this elegant approach actually works:

#!/bin/bash

file=/tmp/test

ping -i 0.1 1.1.1.1 > $file&
sleep 1

tail -f $file | while read ping_line
do
        echo $ping_line
        sleep 0.2
        > $file
done

but on some systems leads to high CPU usage, what would be the direct equivalent using a fifo buffer?

I tried this:

#!/bin/bash

pipe=/tmp/test
mkfifo $pipe

ping -i 0.1 1.1.1.1 > $pipe&
sleep 1

while read ping_line <$pipe
do
        echo $ping_line

        dd if=$pipe iflag=nonblock of=/dev/null
done

What I really want is a last in first out buffer that is reset on every read.

Every read of what? This constraint likely (unnecessarily?) limits alternatives that might work.

The example i post will do this (but you may need to modify it for your situation - skip_interval and the associated if statement accomplishes an analogous function to your sleep 1).

If your willing to compile your own utility, have a look at ftee which may get you closer in concept to what you ask. The link will also show the limitation of using a named pipe (you can't seek to the end of a file like I suspect tail does).

I made a small edit to the script above. If you use a named pipe, you have to "empty" the output from the pipe until you need a value.

Silly question, why not simply do everything sequentially? Rund ping in -c single-shot mode and process the output then sleep until the next desired ping time?

The next best thing would be to patch ping to actually overwrite its result so that there is only ever a single line to parse (the most recent one). The whole have ping append continuously into file/pipe approach really requires a reliable service to truncate that output cyclically or it risks to overrun storage or memory.

Instead of one ping @10Hz run two interleaved at 5Hz each you still get ~0.1ms update intervals but have enough time to process output sequentially, no?

1 Like

I tried the one shot ping but it is very slow.

So the idea is to address the problem simulated as:

#!/bin/bash

ping -i 0.1 1.1.1.1 | while read ping_line
do
        echo $ping_line
        sleep 1
done

in such a way that the ping processed is always the most up to date ping. If the ping processing is fast enough, then this would be every 0.1s. If it is not fast enough, it skips as necessary, but key is that it always processes the latest ping.

This is why I call it a last in first out buffer, because we always want to read the latest available sample. And then we flush the buffer, ready for the next read.

If may also be fine to block ping pending processing, such that ping is restarted whilst processing is finished.

I think fifo buffers in bash allow this, but my understanding of pipes and fifo buffers is not good enough to work out how to use this.

Your code is certainly a solution, but I want to try to make sure a fifo or file based approach is not possible first because the timer based approach is a bit of a cludge.

For example, this blocks, and I don't understand why:

#!/bin/bash

pipe=/tmp/test
mkfifo $pipe

ping -i 0.1 -D 1.1.1.1 > $pipe&
sleep 1

while read ping_line <$pipe
do
        echo $ping_line
        sleep 1
        dd if=$pipe iflag=nonblock of=/dev/null 2> /dev/null
        echo "next loop"
done

I think something along these lines might give what is needed though?

I've also tried permutations like:

#!/bin/bash

pipe=/tmp/test
mkfifo $pipe

stdbuf -oL -eL ping -i 0.1 -D 1.1.1.1 > $pipe&
sleep 1

stdbuf -oL -eL cat $pipe | while read ping_line
do
        echo $ping_line
        sleep 1
        dd if=$pipe iflag=nonblock of=/dev/null 2> /dev/null
        echo "next loop"
done

I have the feeling something like this may work? It needs a solid understanding of the way buffering / pipes / fifo blocks/unblocks works.

lol, IMHO my "solution" not a "bit" of cludge, it's totally a cludge. A more "elegant" solution is to compile your own ping utility as @moeller0 suggests above (ping every n intervals but output either on demand with some kind of signal or based on a timer).

I've seen you try several variants above with a named pipe (aka file descriptor). So did I and I don't think it will work. Check out the link to ftee to understand why.

Best of luck.

Partially offtopic, but was there any specific reason why you specified the non-standard "bash" as the shell for the discussion?

The context is my autorate script here:

It works great on my RT3200, but for some routers the truncation inside the tail -f results in mysterious increased CPU usage with time.

Specifically this:

#!/bin/bash

ping -i 0.1 1.1.1.1 > /tmp/ping_output&

tail -f /tmp/ping_output | while read ping_line

do

        echo $ping_line

        > /tmp/ping_output

done

works, but for some systems results in CPU creep? Is that to do with swapping or something?

Please post the output of:
time /usr/bin/ping -D -c 1 8.8.8.8
on your router, well possible that my router simply is overpowered enough to not care... note that the time obviously also contains the actual RTT, so processing overhead roughly is the real time component minus the RTT. AS long as that temporal overhead is small compared to the actual RTTs I argue that accepting single shot ping might be the easiest path forward.

I would not runs this on a storage/memory limited router that way, at the very least I would do -c 1000 (needs to be measured) to have a worst case bound of the file size... and then restart ping again for the next batch.

But here is the point, if we are only intersted in the most recent data point, why store all the older ones? After all in bash we have no convenient way of doing something useful with the historic data?

How that? The -i 0.1 just moves the timer into the png binary, no?

I have no ideas about shell fifo's but I would not be amazed if the reader actually can not delete without write permissions or similar.

Again why. We want to do something cyclically, so a timer is IMHO exactly the solution to that problem. We can haggle where we put that timer, but the timer seems without a real alternative. I am probably misunderstanding your real point, sorry for that.

What I really like with the shell approach is that it requires no compilation and hence will allow easy modifications as long as a router under test offers a text editor. Also getting delay estimates and not consuming them seems something we should avoid :wink:

1 Like
root@OpenWrt:~/CAKE-autorate# root@OpenWrt:~/CAKE-autorate# time /usr/bin/ping -D -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
[1646743784.198190] 64 bytes from 8.8.8.8: icmp_seq=1 ttl=113 time=42.4 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 42.430/42.430/42.430/0.000 ms
real    0m 0.04s
user    0m 0.00s
sys     0m 0.00s

There is a way to block ping whilst a read is not pending I think using bash named pipes. Wouldn't that work too?

So it is like ping | fifo

ping is blocked, while fifo is not being read. Something like this. I just don't understand well enough how the blocking works.

And a fifo can be flushed using:

dd if=$pipe iflag=nonblock of=/dev/null 2> /dev/null

I feel like a solution here is possible. But from internet searching and reading stackoverflow posts, it seems knowledge in this is pretty limited.

I don't think your misunderstanding and thank you for pointing out the advantages. One limitation I see with my script is that it may come with it's own cpu burden compared to a "fully compiled" solution especially when doing sqm on a "high speed" link.

See this took ~40 milliseconds according to time but the RTT itself is also 42ms, so there was/is almost no set-up cost on your router as well, no? Now the question is how long does it take to massage the data out of that in shell...

To elaborate, even when ping is running constantly the RTT will be 40ms, so the data will not be available for consumption significantly faster, no?

I think only if you modify the ping binary, but if you do that simpler solutions come to mind.

Yes, it is well possible that all that shell flexibility comes at unacceptable CPU costs, but still it seems great for prototyping a solution for more performant implementation later. But I really can not believe that massaging around 10 ping outputs per second in shell should overtax even low performance routers

1 Like

I originally had one shot approach in my GitHub, but it resulted in poor performance.

#!/bin/bash

ping -i 0.1 1.1.1.1 > /tmp/ping_output&

tail -f /tmp/ping_output | while read ping_line

do

        echo $ping_line

        > /tmp/ping_output

done

works, but only for some systems it seems. I don't understand why the truncation results in CPU usage creeping in some systems.

I have this question as well. I was careful to propose a solution that will work with /usr/bin/sh as I've run into issues many times trying to adapt a bash script to run on openwrt (with out having to install bash - awk becomes very useful without bash as well as frustrating).

This also works, but is not satisfactory:

#!/bin/bash

file=/tmp/test

ping -i 0.1 1.1.1.1 | while read line; do echo $line > $file; done&
sleep 1

while true
do
        tail -1 $file
        sleep 1
done
~

For sequential processing, the & seems not needed (a timeout however is needed, but that is implementation dependent).

I really would like to see the time /usr/bin/ping -n -D -c 1 8.8.8.8 output of such a system, not saying these do not exist, but it seems curious why some systems seem to be bothered by this.

So I see a number of features in bash worth ignoring ash for anything complex, bash offers access to arrays and to high resolution timestamps without having to call date.

sh really is not a nice environment (bash is slightly better and offers two features I think quite helpful for the main control loop)

Agreed. I avoid bash on openwrt as it's not a default package (due to it's disk usage I think). For trouble shooting purposes, I like my scripts to run even when bash is unavailable. That said, for this use case, bash is likely to be available.

I think you'd have to test on such a system to answer that. Perhaps runing the tail -f /tmp/ping_output retains the /tmp/ping_output file so it grows inspite of your effort to overwrite it in the while loop.

2 Likes