Using Ping in Parallel in a Bash Script

2020-06-29 05:56发布

So I'm trying to ping a range of addresses in parallel in a bash script, count the ones that are alive, and print them out. The script works to ping the addresses and print the live ones out but it always outputs:

"There were 0 online hosts and 254 offline hosts"

It's not incrementing the ALIVE variable in my code, maybe because it's in a subshell? How could I get around this? Here's what I have right now:

#!/bin/bash

TOTAL=0
ALIVE=0

if [ $# -eq 3 ]
then
    echo -n 'Live hosts:'
    for ((i = $2; i <= $3 && i <= 254; ++i))
    do
        ((++TOTAL))
        ping -c 1 -i 0.2 -w 1 -W 1 $1.$i > /dev/null && ((++ALIVE)) && echo "    $1.$i" &
    done

    echo "There were $ALIVE online hosts and $((($TOTAL - $ALIVE))) offline hosts"
else
    echo "USAGE: pingRange.sh <first 3 octets of ip> <last octet start> <last octet end>"
    echo "    Ex: pingRange.sh 192.168.0 1 254

Note: An example input for the script is shown in the "else" part.

Note 2: Yes I know nmap is easier, I've already wrote a working script with nmap, trying to do one for ping now.

Note 3: I used a temporary file and it worked, updated code has #NEW comment:

#!/bin/bash

if [ $# -eq 3 ]
then
    TOTAL=0 #NEW
    TEMP=mktemp #NEW
    echo -n 'Live hosts:'
    for ((i = $2; i <= $3 && i <= 254; ++i))
    do
        ((++TOTAL))
        ping -c 1 -i 0.2 -w 1 -W 1 $1.$i > /dev/null && echo "    $1.$i" >> $TEMP & #NEW
    done
    wait #NEW
    cat $TEMP

    ALIVE=$(cat $TEMP | wc -l) #NEW
    echo "There were $ALIVE online hosts and $((($TOTAL - $ALIVE))) offline hosts"

    rm $TEMP #NEW
else
    echo "USAGE: pingRange.sh <first 3 octets of ip> <last octet start> <last octet end>"
    echo "    Ex: pingRange.sh 192.168.0 1 254

标签: bash ping
3条回答
家丑人穷心不美
2楼-- · 2020-06-29 06:16

Using GNU Parallel it looks like this:

pingrange() {
  three=$1
  start=$2
  end=$3
  total=$(($end-$start))
  online="$(seq $start $end |
    parallel -j0 "ping -c 1 -i 0.2 -w 1 -W 1 $three.{} > /dev/null && echo '    $three.{}'")"
  alive=$(echo "$online" | wc -l)
  offline=$((total-alive))
  echo "$online"
  echo "There were $alive online hosts and $offline offline hosts"
}

It runs correctly even if your system cannot run all pings in parallel at the same time (e.g. if your process table is near full).

查看更多
smile是对你的礼貌
3楼-- · 2020-06-29 06:18
... && ((++ALIVE)) && ... &

This expression is run inside inside another process, so the changes are not visible in your parent process

a=1
((++a)) &    # <- is run as another process
echo "$a"  # this will print 1 ...

(
    ((++a))
    echo "$a"
) &          # this will print 2 in the background

wait

So, we want to run (( $3 - $2 )) processes. Each of these processes will be run concurrently. We need to get output from all these processes on the end. We arrive at what we call "synchronization". We need to synchronize all values from all the processes to one point.
We can imagine using ex. 255 files, one file unique for each process. Then after childs execute we can query the files.
The easiest is to use stdout or other line-buffered stream:

live_hosts=$(
   for ((i = $2; i <= $3 && i <= 254; ++i)); do
       # `if` is more readable then `a && b`
       (
          if ping -c 1 -i 0.2 -w 1 -W 1 "$1.$i" >/dev/null; then
              echo "$1.$i"
          fi
       ) &
   done
   wait  # remember to wait for all the childs
)

Because stdout should be line buffered, the multiple echo "$1.$i" shouldn't intercept the write, so we should arrive at just a variable with lines. Then you can just:

echo "There were $(printf "$live_hosts" | wc -l) online hosts"

But we could do this with a temporary directory:

tmpdir=$(mktemp -d)

for ((i = $2; i <= $3 && i <= 254; ++i)); do
   (
      if ping -c 1 -i 0.2 -w 1 -W 1 "$1.$i" >/dev/null; then
           # create a file with the name "$i" inside tmpdir
           # I don't think content matters (just the name of file)
           touch "$tmpdir"/"$i"
       fi 
    ) &
done
wait

# ex. the count of alives are the count of files inside out tmpdir
alive=$(find "$tmpdir" -type f -print . | wc -c)

# this is funny
for i in "$tmpdir"/*; do
      echo "$1.$i is alive!"
done

# remember to cleanup
rm -r "$tmpdir"

And just to make it interesting and because we love oneliners, here's a solution using xargs and seq:

live_hosts=$(seq -f "$1.%.0f" "$2" "$3" | xargs -n1 -P0 -- sh -c 'ping -c 1 -i 0.2 -w 1 -W 1 "$1" >/dev/null && echo "$1"' --)
alive=$(echo "$live_hosts" | wc -l)
# well, if just the count matters, add the `| wc -l` to the one liner ..
查看更多
你好瞎i
4楼-- · 2020-06-29 06:22

I believe you can do this just using wait.

Amending your code, something like (untested):

#!/bin/bash

TOTAL=0
ALIVE=0

if [ $# -eq 3 ]
then
    unset pids
    declare -A pids
    echo -n 'Live hosts:'
    for ((i = $2; i <= $3 && i <= 254; ++i))
    do
        ((++TOTAL))
        ping -c 1 -i 0.2 -w 1 -W 1 $1.$i > /dev/null &
        pids[$i]=$!
    done

    for i in "${!pids[@]}"
    do
        wait ${pids[$i]} && ((++ALIVE)) && echo "    $1.$i"
    done

    echo "There were $ALIVE online hosts and $((($TOTAL - $ALIVE))) offline hosts"
else
    # ...
  • unset / declare - just to be safe
  • ping ... & still runs the comand in the background
  • pids[$i]=$! saves its pid
  • for ... loops over the keys
  • wait ${pids[$i]} returns the exit status after cmd has completed
  • && ... does the same as before
查看更多
登录 后发表回答