UPDATE 20180605: 此种方式存在丢任务的情况,用 parallel 命令做多线程更好更简单
shell不能实现多线程,但是可以通过限制几乎同时放入后台执行的进程数量来模拟多线程,从而达到在提高脚本执行效率的同时又不明显增加负载的作用。
Ping脚本的多线程实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#!/bin/bash set -x #开启调试模式 # Usage: # History: # thread=$1 #设置线程数,在这里所谓的线程,其实就是几乎同时放入后台(使用&)执行的进程。 if [ "$1"x == ""x ]; then thread=1 fi tmp_fifofile=/tmp/$$.fifo #脚本运行的当前进程ID号作为文件名 mkfifo $tmp_fifofile #新建一个随机fifo管道文件 exec 6<>$tmp_fifofile #定义文件描述符6指向这个fifo管道文件 rm $tmp_fifofile #清空管道内容 #定义一个函数做为线程(子进程),该函数功能是ping测试 function func() { ping -c 3 $ip &>/dev/null && r=0 || r=1 if [ $r -eq 0 ]; then echo "$ip ok" else echo "$ip failed" fi sleep 3 } # for循环 往 fifo管道文件中写入$thread个空行 for ((i=0;i<$thread;i++));do echo done >&6 # 从ip.txt中读取ip while read ip;do read -u6 #从文件描述符6中读取行(实际指向fifo管道) { func echo >&6 #再次往fifo管道文件中写入一个空行 } & # {} 这部分语句被放入后台作为一个子进程执行,所以不必每次等待3秒后执行 #下一个,这部分的func几乎是同时完成的,当fifo中thread个空行读完后 while循环 # 继续等待 read 中读取fifo数据,当后台的thread个子进程等待3秒后,按次序 # 排队往fifo输入空行,这样fifo中又有了数据,while循环继续执行 done < ip.txt #从ip.txt中读取数据 wait #等到后台的进程都执行完毕 exec 6>&- ##删除文件描述符6 exit 0 |
ip.txt中有9个ip,9个线程,调试模式执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
[root@localhost multhread]# ./thread.sh 9 + thread=9 + '[' 9x == x ']' + tmp_fifofile=/tmp/12088.fifo + mkfifo /tmp/12088.fifo + exec + rm /tmp/12088.fifo + (( i=0 )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + echo + (( i++ )) + (( i<9 )) + read ip + read -u6 + func + ping -c 3 10.217.13.1 + read ip + read -u6 + func + ping -c 3 10.217.13.2 + read ip + read -u6 + func + ping -c 3 10.217.13.3 + read ip + read -u6 + func + ping -c 3 10.217.13.4 + read ip + read -u6 + func + ping -c 3 10.217.13.5 + read ip + read -u6 + func + ping -c 3 10.217.13.6 + read ip + read -u6 + func + ping -c 3 10.217.13.7 + read ip + read -u6 + func + ping -c 3 10.217.13.8 + read ip + read -u6 + func + ping -c 3 10.217.13.9 + read ip + wait + r=0 + '[' 0 -eq 0 ']' + echo '10.217.13.1 ok' 10.217.13.1 ok + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.2 failed' 10.217.13.2 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.3 failed' 10.217.13.3 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.4 failed' 10.217.13.4 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.8 failed' 10.217.13.8 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.9 failed' 10.217.13.9 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.5 failed' 10.217.13.5 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.7 failed' 10.217.13.7 failed + sleep 3 + r=1 + '[' 1 -eq 0 ']' + echo '10.217.13.6 failed' 10.217.13.6 failed + sleep 3 + echo + echo + echo + echo + echo + echo + echo + echo + echo + exec + exit 0 |
执行时间对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[root@localhost multhread]# time ./thread.sh &>/dev/null real 0m53.051s user 0m0.004s sys 0m0.020s [root@localhost multhread]# time ./thread.sh 10 &>/dev/null real 0m6.024s user 0m0.013s sys 0m0.016s [root@localhost multhread]# time ./thread.sh 100 &>/dev/null real 0m6.027s user 0m0.015s sys 0m0.017s [root@localhost multhread]# time ./thread.sh 9 &>/dev/null real 0m6.023s user 0m0.013s sys 0m0.015s |
可以看到线程数量正好合适时执行速度比较快。
CMDB内外网错误修正脚本多线程实现
这是工作中的一个实例,我司的IP分为内网和外网,记录在CMDB中,坑爹的CMDB不校验内外网,可以随便填,于是各种乱象,内网写成外网的,外网写成内网的,还有写“内网IP”,“公网”,还有空着啥都不写的。
CMDB中记录的格式是 :ID,对象类型,IP地址,所属机器盘点号,内外网区分,描述,可以导出为csv文件。基于一个规则文件处理导出的csv数据,找出错误的数据,并纠正,然后在导入CMDB系统。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#!/bin/bash # Usage: # History: # thread=$1 #设置线程数,在这里所谓的线程,其实就是几乎同时放入后台(使用&)执行的进程。 if [ "$1"x == ""x ] || [ "$2"x == ""x ]; then echo "2 args: ./cmdb.sh thread cmdbfile" exit 0 fi CMDB_FILE_NAME=$2 RULES_FILE=rule.txt rm -rf error correct no_rule mkdir error mkdir correct mkdir no_rule tmp_fifofile=/tmp/$.fifo #脚本运行的当前进程ID号作为文件名 mkfifo $tmp_fifofile #新建一个随机fifo管道文件 exec 6<>$tmp_fifofile #定义文件描述符6指向这个fifo管道文件 rm $tmp_fifofile #清空管道内容 #定义一个函数做为线程(子进程) function func() { id=`echo $id | sed "s/\"\",/\"kong\",/g"` #将类型为空的替换为 kong TYPE=`echo $id | awk -F '["]' '{print $10}'` #cmdb中查到的网络类型,必须处理为空的类型 NET=`echo $id | awk -F '["]' '{print $6}' |awk -F '[.]' '{print $1}'` #IP地址前8位 if [ "$NET"x = "10"x ]; then NET=`echo $id | awk -F '["]' '{print $6}' |awk -F '[.]' '{print $1"."$2}'` #10开头的取前16位 if [ "$NET"x = "10.62"x ] || [ "$NET"x = "10.63"x ] || [ "$NET"x = "10.64"x ]; then NET=`echo $id | awk -F '["]' '{print $6}' |awk -F '[.]' '{print $1"."$2"."$3}'` #10.62/63/64开头的取前24位 fi fi ((++$i)) echo "$i - $NET - $TYPE" RULE=`grep "^$NET\." $RULES_FILE | awk '{print $2}'` #对应规则 if [ "$RULE"x = ""x ]; then TMP=`echo $NET | awk -F '[.]' '{print $1}'` if [ "$TMP"x = "10"x ]; then RULE=$TYPE echo $id >>no_rule/$NET.csv else RULE="外网" #规则中没有包含的且不为私有地址的统一作为外网处理,因此公网IP规则不需要写进规则文件 fi fi if [ "$RULE"x != "$TYPE"x ]; then #如果查到的类型和对应规则不符,则输出 echo $id >>error/$NET.csv echo $id |sed "s/$TYPE/$RULE/g" >>correct/$NET.csv fi sleep 3 } # for循环 往 fifo管道文件中写入$thread个空行 for ((i=0;i<$thread;i++));do echo done >&6 #从cmdb.csv中读取 i=1 while read id;do read -u6 #从文件描述符6中读取行(实际指向fifo管道) { func echo >&6 #再次往fifo管道文件中写入一个空行 } & # {} 这部分语句被放入后台作为一个子进程执行,所以不必每次等待3秒后执行 #下一个,这部分的func几乎是同时完成的,当fifo中thread个空行读完后 while循环 # 继续等待 read 中读取fifo数据,当后台的thread个子进程等待3秒后,按次序 # 排队往fifo输入空行,这样fifo中又有了数据,while循环继续执行 ((i++)) done < $CMDB_FILE_NAME #从cmdb file中读取数据 wait #等到后台的进程都执行完毕 exec 6>&- ##删除文件描述符6 exit 0 |
优化方案
上面的代码可以解决问题,但是速度太慢了,大约要30分钟。数据总量近10万条,错误的占总数并不多,但是脚本要一条条的去检查然后比对规则。因此如果能把错误的先找出来,在用上面的脚本处理几千条错误的,速度就能快很多。
改用grep,加-v选项,能实现错误的秒级查找,然后用上面的脚本纠错,也是几秒钟的事情,整个过程不到1分钟就能完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#!/bin/bash # Usage: # History: # set -x if [ $# != 2 ];then echo "args error" exit 1 fi rm -f error.csv touch error.csv rm -rf tmp mkdir tmp RULEFILE="$1" CMDBFILE="$2" id=1 cp $CMDBFILE tmp/tmp_$id while read line do NET=`echo $line |awk '{print $1}' |sed 's/\./\\\./g'` RULE=`echo $line |awk '{print $2}'` if [ "$NET"x = ""x ]; then NET="NULLOFRULE" fi NET="\\\"$NET" RULE="\\\"$RULE\\\"," #逗号必加,处理将内网网写到描述中去的情况 grep -E ".+$NET.+" $CMDBFILE |grep -E -v ".+$NET.+$RULE.*" >> error.csv grep -E -v "$NET" tmp/tmp_$id >tmp/tmp_file ((id++)) mv tmp/tmp_file tmp/tmp_$id done <$RULEFILE grep -E "\"10\." tmp/tmp_$id >no_rules.csv grep -E -v "\"10\." tmp/tmp_$id |grep -E -v ".+\"外网\".*" >public_error.csv |
参考资料
1 2 3 |
[1]. SHELL模拟多线程脚本的详细注解.http://blog.sina.com.cn/s/blog_65d6476a01017t7f.html [2]. 管道技巧-while read line.http://blog.csdn.net/hunanchenxingyu/article/details/9998089 [3]. Linux shell 实现多线程. http://llystar.iteye.com/blog/1189486 |
例子
#!/bin/bash
chan=$(mktemp -u /tmp/para.$$.XXXXXXXXX)
mkfifo $chan
exec 6<>$chan
rm $chan
task() {
echo "$(date) - Start task $1"
sleep 3
}
for i in {0..5};do
echo
done >&6
for i in {0..10};do
read -u6
{
task $i
echo >&6
} &
done
wait
exec 6>&-
执行结果
Fri Jul 16 02:03:59 CST 2021 - Start task 2
Fri Jul 16 02:03:59 CST 2021 - Start task 0
Fri Jul 16 02:03:59 CST 2021 - Start task 1
Fri Jul 16 02:03:59 CST 2021 - Start task 3
Fri Jul 16 02:03:59 CST 2021 - Start task 4
Fri Jul 16 02:04:00 CST 2021 - Start task 5
Fri Jul 16 02:04:03 CST 2021 - Start task 6
Fri Jul 16 02:04:03 CST 2021 - Start task 7
Fri Jul 16 02:04:03 CST 2021 - Start task 10
Fri Jul 16 02:04:03 CST 2021 - Start task 8
Fri Jul 16 02:04:03 CST 2021 - Start task 9
补充参考资料
https://taoyan.netlify.app/post/2020-01-02.%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97/
TLCL 36章,具名管道