返回顶部

zhangfd

个人博客,仅供学习使用

导航

shell编程 10 --- while 和until 循环的应用实践

shell编程 10 --- while 循环和until 循环的应用实践


循环语句命令常用于重复执行一条指令或一组指令,直到条件不再满足时停止,
Shell脚本语言的循环语句常见的有while、until、for及select循环语句。
while循环语句主要用来重复执行一组命令或语句,在企业实际应用中,
常用于守护进程或持续运行的程序,除此以外,大多数循环都会用后文即将讲解的for循环语句。


10.1 当型和直到型循环语法

10.1.1 while循环语句

while循环语句的基本语法为:

while 条件表达式
do
	指令
done

说明:


while循环语句会对紧跟在while命令后的条件表达式进行判断,如果该条件表达式成立,则执行while循环体里的命令或语句(即语法中do和done之间的指令),每一次执行到done时就会重新判断while条件表达式是否成立,直到条件表达式不成立时才会跳出while循环体。如果一开始条件表达式就不成立,那么程序就不会进入循环体(即语法中do和done之间的部分)中执行命令了。


while循环执行流程对应的逻辑图如图:

10.1.2 until循环语句

until循环语句的语法为:

until 条件表达式
do
	指令..
done

说明:

until循环语句的用法与while循环语句的用法类似,区别是until会在条件表达式不成立时,进入循环执行指令;条件表达式成立时,终止循环。

10.2 当型和直到型循环的基本规范


首先来了解一下Shell中的两个休息命令:
sleep 1表示休息1秒,usleep 1000000也表示休息1秒。


10.2.1 每隔2秒输出一次系统负载

每隔2秒输出一次系统负载(负载是系统性能的基础重要指标)情况。
参考答案1:每隔2秒在屏幕上输出一次负载值。

[root@zabbix 0509]# cat while_load.sh 
#!/bin/bash
while true	
do
    uptime
    sleep 2
done
## while true 会一直循环,因此叫做守护进程
## sleep 2 间隔2秒后继续循环,目的是控制循环的频率,否则会消耗大量系统资源,成为死循环

参考答案2:将负载值追加到log里,使用微秒单位。

[root@zabbix 0509]# cat while_uptime.sh 
#!/bin/bash
while [ 1 ]	
do
    uptime >>/tmp/uptime.log
    usleep 2000000
done
## while [ 1 ] = while true 
## usleep 2000000=sleep 2 

通过在脚本的结尾使用&符号来在后台运行脚本:

[root@zabbix 0509]# sh while_uptime.sh &
[1] 3172
[root@zabbix 0509]# tail -f /tmp/uptime.log 
 12:39:13 up  3:56,  1 user,  load average: 0.00, 0.01, 0.05
 12:39:15 up  3:56,  1 user,  load average: 0.00, 0.01, 0.05
^C
[root@zabbix 0509]# ps -ef|grep while_uptime
root       3172   1614  0 12:39 pts/0    00:00:00 sh while_uptime.sh
[root@zabbix 0509]# kill -9 3172


在实际工作中,一般会通过客户端SSH连接服务器,因此可能就会有在脚本或命令执行期间不能中断的需求,若中断,则会前功尽弃,更要命的是会破坏系统数据。防止脚本执行中断的几个可行方法:

1)使用sh /server/scripts/while_01.sh &命令,即使用&在后台运行脚本。
2)使用nohup /server/scripts/uptime.sh &命令,即使用nohup加&在后台运行脚本。
3)利用screen保持会话,然后再执行命令或脚本,即使用screen保持当前会话状态。
此外,让进程在后台可靠运行的几种方法的参考资料如下:
http://www.ibm.com/developerworks/cn/linux/l-cn-nohup/


10.3 让shell脚本在后台运行的知识

10.3.1 脚本运行的相关用法和说明

用法 说明
sh while.sh & 把脚本while.sh放到后台执行(在后台运行脚本时常用的方法)
ctl + c 停止执行当前脚本或任务
ctl + z 暂停执行当前脚本或任务
bg 把当前脚本或任务放到后台执行,bg可以理解为background
fg 把当前脚本或任务放到前台执行,如果有多个任务,
可以使用fg加任务编号调出对应的脚本任务,
如fg2,是指调出第二个脚本任务,fg可以理解为frontground
jobs 查看当前执行的脚本或任务,能查看到任务编号
kill 关闭执行的脚本任务,即以"kill %任务编号" 的形式关闭脚本,
这个任务编号可以通过jobs来获得

表中知识进行实践演示:

[root@zabbix 0509]# sh while_uptime.sh &   ----结尾使用&表示在后台运行
[1] 3339
[root@zabbix 0509]# fg			  --- fg加jobs中的任务编号,调出对应脚本到前台执行
sh while_uptime.sh
^Z											-- ctl + z 暂停执行
[1]+  Stopped                 sh while_uptime.sh
[root@zabbix 0509]# bg						-- 将当前执行的脚本放到后台执行
[1]+ sh while_uptime.sh &
[root@zabbix 0509]# jobs					-- 查看当前shell下运行的脚本任务
[1]+  Running                 sh while_uptime.sh &
[root@zabbix 0509]# fg 1		--- fg加jobs中的任务编号,调出对应脚本到前台执行
sh while_uptime.sh
^C								ctl + c 停止执行
[root@zabbix 0509]# jobs
[root@zabbix 0509]# sh while_uptime.sh &
[1] 3411
[root@zabbix 0509]# sh while_uptime.sh &
[2] 3416
[root@zabbix 0509]# jobs
[1]-  Running                 sh while_uptime.sh &
[2]+  Running                 sh while_uptime.sh &
[root@zabbix 0509]# kill %2			---- kill命令关闭jobs任务脚本
[root@zabbix 0509]# jobs
[1]-  Running                 sh while_uptime.sh &
[2]+  Terminated              sh while_uptime.sh
[root@zabbix 0509]# 

更多有关进程管理的Linux相关命令如下:

  1. kill、killall、pkill:杀掉进程。
  2. ps:查看进程。
  3. pstree:显示进程状态树。
  4. top:显示进程。
  5. renice:改变优先权。
  6. nohup:用户退出系统之后继续工作。
  7. pgrep:查找匹配条件的进程。
  8. strace:跟踪一个进程的系统调用情况。
  9. ltrace:跟踪进程调用库函数的情况。

while和until范例:

#!/bin/bash
i=5
#while((i>0))
#while [[ $i > 0 ]]
#while [ $i -gt 0 ]
#until [[ $i < 1 ]]
until [[ $i < 0 || $i = 0 ]]
do
    echo $i
    ((i--))
done

10.3.2 while:计算从1加到100之和

  1. 通过while循环实现:
[root@zabbix 0509]# cat while_sum.sh 
#!/bin/bash
i=1
sum=0
while ((i<=100))
do
   ((sum+=$i))
   ((i++))
done
[ "$sum" -ne 0 ] && printf "total sum is : $sum\n"
  1. 通过数学求和公式实现
[root@zabbix 0509]# i=100
[root@zabbix 0509]# ((sum=i*(i+1)/2))
[root@zabbix 0509]# echo $sum
5050
# 使用求和公式,代码简单而且运算高效

10.3.3 猜数字游戏


首先让系统随机生成一个数字,给这个数字设定一个范围(1~60),让用户输入所猜的数字。游戏规则是:对输入进行判断,如果不符合要求,就给予高或低的提示,猜对后则给出猜对所用的次数,请用while语句实现。


[root@zabbix 0509]# cat while_game.sh 
#!/bin/bash
total=0
export LANG="zh_CN.UTF-8"
NUM=$((RANDOM%61))
echo "current price of apple is $NUM per weight."
echo "======================"
usleep 10000
clear
echo 'how much noney of the apples?
      from 0 to 60'
apple(){
    read -p "input your price num:" PRICE
    expr $PRICE + 1 &>/dev/null
    if [ $? -ne 0 ]
      then
	echo "no kidding,guess the num is :"	
	apple
    fi
}
guess(){
    ((total++))
    if [ $PRICE -eq $NUM ]
      then 
	echo "You right . that is $NUM dollor."
	if [ $total -le 3 ];then
	    echo "you just guessed $total times,that's great."
        elif [ $total -gt 3 -a $total -le 6 ];then
	    echo "you have guessed even $total times,good."
	else
	    echo "$total times,so hard? guys.."
	fi
	exit 0
      elif [ $PRICE -gt $NUM ];then
	echo "It's too higher,once more:"
	apple
      else	
	echo "It's too lower,keep going,you can do it.again:"
	apple
    fi
}
main(){
   apple
   while true
   do
	guess
   done
}
main
## 如果使用中文,注意设置字符集LANG

10.3.4 手机发短信功能及充值提醒


手机充值10元,每发一次短信(输出当前余额)花费1角5分钱,当余额低于1角5分钱时就不能再发短信了,提示“余额不足,请充值”(允许用户充值后继续发短信),请用while语句实现。
在解答之前,先进行单位换算,统一单位,让数字变成整数,即:10元=1000分,1角5分=15分


实现代码参考:

[root@zabbix 0509]# cat while_message.sh 
#!/bin/bash
export LANG="zh_CN.UTF-8"
sum=15
msg_fee=15
msg_count=0

menu(){
    cat <<zfd
  当前余额为${sum}分,每条短信需要${msg_fee}分
  ===========================================
      1.充值
      2.发消息
      3.退出
  ===========================================
zfd
}
recharge(){
    read -p "请输入充值金额:" money
    expr $money + 1 &>/dev/null
    if [ $? -ne 0  ]
      then
	echo "input error, please input integer count."
	exit
    else 
	sum=$(($sum+$money))
	echo "充值成功!当前余额为:$sum"
    fi
}
sendInfo(){
    if [ $sum -lt $msg_fee ]
      then
	printf "余额不足:$sum ,请充值"
    else
	while true
	do
	    read -p "请输入短信内容(不能有空格,0:退出):" msg_cont
	    msg=${msg_cont}
	    [ -z "$msg" ] && {
		echo "请重新输入短信内容"
		return 2
	    }
	    [ $msg_cont -eq 0 ] && return 2 
	    printf "Send `echo -n $msg` successful.\n"
	    sum=$(($sum-$msg_fee))	
	    printf "当前余额为:$sum\n"
	    if [ $sum -lt $msg_fee ]
	      then
		printf "余额不足,剩余 $sum 分\n"
		return 1
	    fi
	    printf "退出短信发送,请按 0 \n"
	done  
    fi
}
main(){
    while true
    do
        menu
	read -p "请输入数字选择:" men
        case $men in
	    1)
		recharge
		;;
	    2)
		sendInfo
		;;
	    3)
		exit 1
		;;
	    *)
		printf "选择错误,请重新选择:{1|2|3}"
	esac

    done
}
main

10.4 while循环语句企业实践


提示:
实际使用时,一些基础的函数脚本(例如:加颜色的函数)是放在函数文件里的,例如放在/etc/init.d/functions里,与执行的内容部分相分离,这看起来更清爽,大型的语言程序都是这样开发的。


10.4.1 while守护进程监控网站


使用while守护进程的方式监控网站,每隔10秒确定一次网站是否正常。


参考代码1:引入函数库并且采用模拟用户访问的方式。

[root@zabbix 0510]# cat while_watch.sh 
#!/bin/bash
. /etc/init.d/functions		## 引入函数库
if [ $# -ne 1 ];then
    echo $"USAGE: $0 url"
    exit 1
fi
while true
do
    if [ `curl -o /dev/null --connect-timeout 5 -s -w "%{http_code}" $1|egrep -w "200|301|302"|wc -l` -ne 1 ];then
    	action  $"$1 is error.`date +%F%T`" /bin/false  ## 显得更专业
    else
	action $"$1 is OK.`date +%F%T`" /bin/true 
    fi
    sleep 5
done
-------------------------------------------------------------------------
[root@zabbix 0510]# sh while_watch.sh www.baidu.com
www.baidu.com is OK.    2020-05-1017:36:37                 [  OK  ]
www.baidu.com is OK.    2020-05-1017:36:42                 [  OK  ]
www.baidu.com is OK.    2020-05-1017:36:48                 [  OK  ]

参考代码2:采用Shell数组的方法,同时检测多个URL是否正常,并给出专业的展示效果,这是实际工作中所用的脚本。

[root@zabbix 0510]# cat while_url.sh 
#!/bin/bash
# this script is creacted by moox.
# e_mail:2144865225@qq.com
# function:case example
# version:1.2

. /etc/init.d/functions
check_count=0

# define array list of url will be checked.
url_list=(
http://blog.moox.com
http://bbs.moox.com
http://www.moox.com
http://172.16.1.71
www.baidu.com
)
# define the function of wait :3,2,1,begin..
function wait(){
    echo -n  '5 seconds later,check URL operation will be run:'
    for ((i=5;i>0;i--))
    do
	echo -n "..${i}..";sleep 1
    done
    echo 
}
#
function check_url(){
    wait 
    for ((i=0;i<`echo ${#url_list[*]}`;i++))
    do
	wget -o /dev/null -T 3 --tries=1 --spider ${url_list[$i]} >/dev/null
    	if [ $? -eq 0 ];then
	    action $"${url_list[$i]}" /bin/true
	else
	    action $"${url_list[$i]}" /bin/false
	fi
    done
    ((check_count++))
}
main(){
    while true
    do
	check_url
	echo "--------check count:${check_count}-----------"
	sleep 10
    done
}
main
[root@zabbix 0510]# 
[root@zabbix 0510]# sh while_url.sh 
5 seconds later,check URL operation will be run:..5....4....3....2....1..
http://blog.moox.com                                       [FAILED]
http://bbs.moox.com                                        [FAILED]
http://www.moox.com                                        [  OK  ]
http://172.16.1.71                                         [  OK  ]
www.baidu.com                                              [  OK  ]
--------check count:1-----------
5 seconds later,check URL operation will be run:..5....4....3....2....1..

10.4.2 分析Apache访问日志


分析Apache访问日志,把日志中每行的访问字节数对应的字段数字相加,计算出总的访问量。给出实现程序,请用while循环实现。(3分钟)


本题要讲解的知识点是利用while循环读取文件操作的方法。根据题意可知,在Web日志里有一列记录了访问资源的大小,把这些资源的大小相加即为本题的答案。

参考代码:这里采用while循环与bash exec内置命令功能配合完成示例。

[root@zabbix 0510]# cat while_apache.sh 
#!/bin/bash
sum=0				# 初始化资源大小总和为0
exec < $1			# 将传参$1 输入重定向给exec
while read line		# 按行读取传参的文件内容
do
    size=`echo $line|awk '{print $10}'`		# 获取每行的第10列,即访问字节的列
    expr $size + 1 &>/dev/null				# 数字判断	
    if [ $? -ne 0 ];then					# 非数字,则执行continue终止本次循环
	continue								# 不计入总和
    fi
    ((sum=sum+$size))						# 字节总和
done
											# 循环求和结束,打印结果
echo "${1} : total : ${sum} bytes = `echo $((${sum}/1024))`KB"

-------------------------------------------------------------------------
[root@zabbix 0510]# head -2 /var/log/apache/access.log-20200510 
172.16.1.71 - - [10/May/2020:16:56:25 +0800] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
172.16.1.71 - - [10/May/2020:16:59:48 +0800] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
[root@zabbix 0510]# sh while_apache.sh /var/log/apache/access.log-20200510
/var/log/nginx/access.log-20200510 : total : 33048 bytes = 32KB
## 补充:非while循环实现
[root@zabbix 0510]# awk '{print $10}' /var/log/nginx/access.log-20200510|awk '{sum+=$1}END{print sum}'
33048

10.5 while循环按行读取文件的方式

while循环按行读文件的几种常见方式:

10.5.1 采用exec读取文件

方式1:采用exec读取文件,然后进入while循环处理。

exec < FILE
sum=0
while read line
do
	cmd
done

10.5.2 采用cat读取文件

方式2:使用cat读取文件内容,然后通过管道进入while循环处理。

cat FILE_PATH|while read line
do
	cmd
done

10.5.3 采用done< 读取文件

方式3:在while循环结尾done处通过输入重定向指定读取的文件

while read line
do
	cmd
done<FILE

实例:开发一个Shell脚本实现Linux系统命令cat读文件的基本功能

[root@zabbix 0510]# cat while_cat.sh 
#!/bin/bash
cat $1|while read line
do
	echo $line
done
[root@zabbix 0510]# sh while_cat.sh /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
172.16.1.5 lb01
172.16.1.6 lb02
...
其余方法
-------------------------------------------------
while read line
do
	echo $line
done < $1
-------------------------------------------------
exec < $1
while read line
do
	echo $line
done

10.6 企业级生产高级实战案例-- DDoS攻击


范例10-10:写一个Shell脚本解决类DDoS攻击的生产案例。请根据Web日志或系统网络连接数,监控某个IP的并发连接数,若短时内PV达到100,即调用防火墙命令封掉对应的IP。防火墙命令为:“iptables -I INPUT -s IP地址-j DROP”。


参考答案1:

先分析Web日志,可以每分钟或每小时分析一次,这里给出按小时处理的方法。可以将日志按小时进行分割,分成不同的文件,根据分析结果把PV数高的单IP封掉。例如,每小时单IP的PV数超过500,则即刻封掉,这里简单地把日志的每一行近似看作一个PV,实际工作中需要计算实际页面的数量,而不是请求页面元素的数量,另外,很多公司都是以NAT形式上网的,因此每小时单IP的PV数超过多少就会被封掉,还要根据具体的情况具体分析,本题仅给出一个实现的案例,读者使用时需要考虑自身网站的业务去使用。

参考答1:

[root@zabbix 0510]# cat while_ddos.sh 
#!/bin/bash
file=$1
while true
do
    awk '{print $1}' $1|grep -v "^$"|sort|uniq -c >/tmp/ddos.log
    exec < /tmp/ddos.log
    while read line
    do
	ip=`echo $line|awk '{print $2}'`
	count=`echo $line|awk '{print $1}'`
	if [ $count -gt 500 ]&& [ `iptables -L -n|grep "$ip"|wc -l` -lt 1 ];then
	    iptables -I INPUT -s $ip -j DROP
	    echo "$line is dropped." >>/tmp/droplist_$(date +%F).log
	fi
    done
    sleep 3600
done
[root@zabbix 0510]# sh while_ddos.sh access.log
...
# 单独打开窗口查看iptables的情况,结果如下:
[root@zabbix 0510]# iptables -L -n

参考答案2:

分析Linux系统的网络连接数,而不是分析Web日志。
设计思路:首先要分析单IP占网络连接数的情况,即取当前网络连接状态为ESTABLISHED的行数,然后分析对应客户端列不同IP连接数量的排序,对排序比较高的IP进行封堵。

[root@zabbix 0510]# cat while_ddos2.sh 
#!/bin/bash
file=$1
JudgeExt(){
    if expr "$1" : ".*\.log" &>/dev/null
      then
	:
    else
	echo $"usage:$0 xxx.log"
	exit 1
    fi
}
IpCount(){
    grep "ESTABLISHED" $1|awk -F "[ :]+" '{ ++S[$(NF-3)]}END {for(key in S) print S[key],key}'|sort -rn -k1|head -5 >/tmp/ddos2.log
}
ipt(){
    local ip=$1
    if [ `iptables -I -n|grep "$ip"|wc -l` -lt 1 ];then
	iptables -I INPUT -s $ip -j DROP
	echo "$line is dropped" >>/tmp/droplist_$(date +%F).log
    fi
}
main(){
     JudgeExt $file
     while true
     do
	IpCount $file
	while read line
	do
	    ip=`echo $line|awk '{print $2}'`
	    count=`echo $line|awk '{print $1}'`
	    if [];then
	        ipt $ip
	    fi
	done </tmp/ddos2.log
	sleep 180
     done
}
main

其他实战题目见“天津项目实践抓阄题目”:http://oldboy.blog.51cto.com/2561410/1308647

10.7 while循环总结

(1)While循环结构及相关语句综合实践小结

  1. while循环的特长是执行守护进程,以及实现我们希望循环持续执行不退出的应用,适合用于频率小于1分钟的循环处理,其他的while循环几乎都可以被后面即将要讲到的for循环及定时任务crond功能所替代。
  2. case语句可以用if语句来替换,而在系统启动脚本时传入少量固定规则字符串的情况下,多用case语句,其他普通判断多用if语句。
  3. 一句话场景下,if语句、for语句最常用,其次是while(守护进程)、case(服务启动脚本)。

(2)Shell脚本中各个语句的使用场景

  1. 条件表达式,用于简短的条件判断及输出(文件是否存在,字符串是否为空等)

  2. if取值判断,多用于不同值数量较少的情况。

  3. for最常用于正常的循环处理中。

  4. while多用于守护进程、无限循环(要加sleep和usleep控制频率)场景。

  5. case多用于服务启动脚本中,打印菜单可用select语句,不过很少见,一般用cat的here文档方法来替代。

函数的作用主要是使编码逻辑清晰,减少重复语句开发。

特别说明:查看本文核心脚本代码

posted on 2020-05-10 22:13  zhangfd  阅读(1247)  评论(0编辑  收藏  举报