UPDATE 20180605: 此种方式存在丢任务的情况,用 parallel 命令做多线程更好更简单
shell不能实现多线程,但是可以通过限制几乎同时放入后台执行的进程数量来模拟多线程,从而达到在提高脚本执行效率的同时又不明显增加负载的作用。
Ping脚本的多线程实现
#!/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 [1]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个线程,调试模式执行结果
[root@localhost multhread]# ./thread.sh 9 + thread=9 + '[' 9x == x ']' + tmp_fifofile=/tmp/12088.fifo + mkfifo /tmp/12088.fifo + exec + rm /tmp/12088.fifo + [2] i=0 + [3] i<9 + echo + [4] i++ + [5] i<9 + echo + [6] i++ + [7] i<9 + echo + [8] i++ + [9] i<9 + echo + [10] i++ + [11] i<9 + echo + [12] i++ + [13] i<9 + echo + [14] i++ + [15] i<9 + echo + [16] i++ + [17] i<9 + echo + [18] i++ + [19] i<9 + echo + [20] i++ + [21] 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
执行时间对比
[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系统。
代码实现
#!/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 [22]++$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 [23]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循环继续执行 [24]i++ done < $CMDB_FILE_NAME #从cmdb file中读取数据 wait #等到后台的进程都执行完毕 exec 6>&- ##删除文件描述符6 exit 0
优化方案
上面的代码可以解决问题,但是速度太慢了,大约要30分钟。数据总量近10万条,错误的占总数并不多,但是脚本要一条条的去检查然后比对规则。因此如果能把错误的先找出来,在用上面的脚本处理几千条错误的,速度就能快很多。
改用grep,加-v选项,能实现错误的秒级查找,然后用上面的脚本纠错,也是几秒钟的事情,整个过程不到1分钟就能完成。
#!/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
[25]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]. 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章,具名管道