参考资料和书籍:
唯一的一篇,至少是我所看到的,中文的RRDTool教学的文章。尽管是繁体字版的,但是确实给了很多的启发和思考。相当好的一篇文章。
2.官方文档 http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/
无论怎么样,英文也好,这个永远是最权威的文档。有不明白的可以查这里。
3.《Unix Shell Programming》第三版
尽管abelyang在《rrdtool 教學》中认为Shell编程是用好RRD的基础,但是我并不这样认为。但是用好Shell,确实对配置有很大帮助。这本书确实很好,入门教材。
一、废话和下载
出于尊重作者Tobi Oetiker的目的,哈哈,我们还是要简要介绍一下这个RRD的。
MRTG和RRD是同一个家族的产品。都是在产生 time-series (时间连续)的图文件(如流量,负载,温度,人数.....)。我们这里用它来产生网络流量图。
MRTG功能比较少,我们在日常使用中也希望对采集时间和采集数据进行更加灵活的表现。所以作者又开发了RRDTool。RRD的全称叫做(Acronym for Round Robin Database)。
一起来看一下作者是如何解释的(可以跳过):
It stores the data in a very compact way that will not expand over time, and it presents useful graphs by processing the data to enforce a certain data density. It can be used either via simple wrapper scripts (from shell or Perl) or via frontends that poll network devices and put a friendly user interface on it.
我们再一起看看,用这两个工具对同一个网络端口流量检测图的对比:
MRTG检测的网络流量
RRD检测的网络流量
官方下载页: http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/download.html
我们这里使用的是:rrdtool-1.0.49.tar.gz
注:2005年6月,RRD已经推出了1.2版本,其中可以使用TrueType字体。但是这里,我们仍然采用旧的版本,总体思想是一样的。
二、在FreeBSD下的安装
1.解压tar包
$ tar zxvf rrdtool-1.0.49.tar.gz // 解压到当前目录下
2.编译和安装
$ ./configure --prefix=/usr/local // 设定RRDTool的安装目录
$ make
$ make install
3.安装完毕后,可以看到在/usr/local中添加了一个rrd目录
4.进入到/usr/local/rrd/bin 中,敲入:
$ ./rrdtool
Usage: rrdtool [options] command command_options
Valid commands: create, update, graph, dump, restore,
last, info, fetch, tune, resize, xport
RRDtool is distributed under the Terms of the GNU General
Public License Version 2. (www.gnu.org/copyleft/gpl.html)
For more information read the RRD manpages
如果可以看到上述提示,恭喜喽,安装成功,可以进入下一步了。
三、RRD的运作流程
参考了很多文档,包括官方文档都没有讲到RRD整体程序的流程,偶觉得这个是很有必要的。对于了解RRD的工作原理是很有帮助的。我们以监控网络流量为例。
① 通过Shell脚本,配置我们监控的端口变量,并生成
.rrd的数据文档。(具体操作和含义后面会具体讲)
(也有人称之为log文档)
② 由数据更新脚本(NIC_7609.sh),通过调用snmpwalk更新
.rrd数据文档。
③ 数据更新脚本(NIC_7609.sh)调用绘图脚本(NIC_7609_Graph.sh)根据
.rrd数据文档,重新绘图。
④ 休息300S,继续 ②
这样,整个流程就说清楚了,下面就根据流程逐步展开。累……
注:因为官方文档是英文的,翻译过程毕竟存在语言差异,所以在一些特别是概念性很强的地方,偶会给出中文的解释和原官方文档的对照。也许英文原文还更好理解。
四、建立RRD数据文档(
.rrd文件)
1.rrdtool 建档语法
rrdtool create filename
[--start|-b start time]
[--step|-s step]
[DS:ds-name:DST:heartbeat:min:max]
[RRA:CF:xff:steps:rows]
用一个官方的例子说明(以下都用这个例子说明参数的使用):
rrdtool create target.rrd
--start 1023654125
--step 300
DS:mem:GAUGE:600:0:671744
RRA:AVERAGE:0.5:12:24
RRA:AVERAGE:0.5:288:31
简单的先说吧:
Create 很容易理解喽,即建档
filename习惯上会以 .
rrd 结尾,记住这个数据文档的名字。
--start 这个 filename 的数据记录起始日期 ,以 1970 年至今的秒数 (如果不设置的话,默认是现在)
--step 采集数据的间格时间,习惯上我们会设 300 (秒),当然可以调整,这才是RRD的优势所在。但是采集周期不应该过短,否则会造成服务器负载过重。
原文相关对照:
This example creates a database named target.rrd.
Start time (1023654125) is specified in total number of seconds since epoch (time in seconds since 01-01-1970). While updating the database, update time is also specified. This update time MUST occur after start time and MUST be in seconds since epoch.
The step of 300 seconds indicates that database expects new values every 300 seconds. The wrapper script should be scheduled to run every step seconds so that it updates the database every step seconds.
后面两个参数是比较大条的,呵呵,所以要慢慢理解喽……
2.DS (Data Source 申明数据源的意思)
DS是用来申明数据源的,也可以理解为申明数据变量,也就是你要检测的端口对应的变量名,这个参数在画图的时候还要使用的。
官方英文解释是这样的:
DS (Data Source) is the actual variable which relates to the parameter on the device that has to be monitored.
语法:DS :
variable_name
:
DST
:
heartbeat
:
min :
max
① DS是关键字。
②
variable_name 是在数据文档中记录对应的变量名。当每一个刷新周期到来的时候,数据文档中各变量对应的值就会被更新。这个变量对应的值在官方文档中也叫做主要数据点――PDP(Primary Data Point)。
③ DST:DS的类型,有四个可选项:
COUNTER, DERIVE, ABSOLUTE, GAUGE
我们用的最多的就是GAUGE了,它的中文解释是:测量。在这里它表示实际的值。而COUNTER表示的是经过一个刷新周期的变化率。剩下两个用的不过,我们也不介绍了。同样的给出一个官方的解释:
COUNTER will save the rate of change of the value over a step period. This assumes that the value is always increasing (difference between last two values is more than 0). Traffic counters on a router is an ideal candidate for using COUNTER as DST.
DERIVE is same as COUNTER but it allows negative values as well. If you want to see the rate of change in free diskspace on your server, then you might want to use the DERIVE data type.
ABSOLUTE also saves the rate of change but it assumes that previous value is set to 0. The difference between current and previous value is always equal to the current value. So, it stores the current value divided by step interval (300 seconds in our example).
GAUGE does not save the rate of change. It saves the actual value itself. There are no divisions/calculations. Memory consumption in a server is an ideal example of gauge.
下面这个例子很好说明了这个问题:
Values = 300, 600, 900, 1200 实际值
Step = 300 seconds 刷新周期
COUNTER DS = 1, 1, 1, 1 COUNTER定义的DS的值
DERIVE DS = 1, 1, 1, 1 DERIVE定义的DS的值
ABSOLUTE DS = 1, 2, 3, 4 ABSOLUTE定义的DS的值
GAUGE DS = 300, 600, 900, 1200 GAUGE定义的DS的值
④
heartbeat 心跳有效期
比如在例子中,我们定义了心跳有效时间是600秒,也就是两个刷新周期。如果,在两个刷新周期内,都没有接收到数据更新,那么这个时候,必须往数据文档中写入一个(UN)UNKNOWN值。这是RRDTool的一个特别的地方。要知道MRTG在处理网络中断的时候,记录的是0值。这个0和UN还是有一定区别的。
官方原文如下:
In our example, heartbeat is 600 seconds. If database does not get a new PDP within 300 seconds, it will wait for another 300 seconds (total 600 seconds). If it doesnt receive any PDP with in 600 seconds, it will save an UNKNOWN value into database. This UNKNOWN value is a special feature of RRDTool - it is much better than to assume a missing value was 0 (zero). For example, the traffic flow counter on a router keeps on increasing. Lets say, a value is missed for an interval and 0 is stored instead of UNKNOWN. Now when next value becomes available, it will calculate difference between current value and previous value (0) which is not correct. So, inserting value UNKNOWN makes much more sense here.
⑤
min :
max 记录数据的最小值和最大值
DS数值的有效范围,超出就是UN喽。也可以不限制用U
3.RRA(Round Robin Archive)
这个我也不知道,该如何给一个中文的翻译。它的作用就是定义更新的数据是如何记录的。比如我们每5分钟产生一条刷新的数据,那么一个小时就是12条。每天就是288条。这么庞大的数据量,一定不可能都存下来。肯定有一个合并(consolidate)数据的方式,那么这个就是RRA的作用了。
语法:RRA :
CF :
xff :
step :
rows
① RRA用以声明RRAs的关键字
② CF(consolidation function)合并方式,有几个可选项:
AVERAGE, MINIMUM, MAXIMUM, LAST 。
上面说过了,经过一个刷新周期,会获得一个主数据点(PDP),将若干个PDPs使用合并方式(CF)合并后会产生一个合并数据点CDP(consolidated data point)。
③ xff:xfiles factor 和unkown数据有关,很多资料都取0.5
④ step:有step条PDP合并形成一条CDP
⑤ row:记录的合并数据点CDP条数。
对应官方的描述为:
RRA is the keyword to declare RRAs. The consolidation function (CF) can be AVERAGE, MINIMUM, MAXIMUM, and LAST. The concept of the consolidated data point (CDP) comes into the picture here. A CDP is CFed (averaged, maximum/minimum value or last value) from
step number of PDPs. This RRA will hold
rows CDPs.
我们在例子中对RRA是这样定义的:
RRA:AVERAGE:0.5:12:24
RRA:AVERAGE:0.5:288:31
对于第一个RRA,12条的PDP(每经过一个刷新周期产生一个PDP)经过CFed(AVERAGE),也就是取平均值,产生一个CDP,24个CDPs存档。我们一起来计算一下时间,如果一个周期是300秒,那么12个PDP的产生时间就是1一个小时,也就是一个小时产生一个CDP。24个CDPs时间就是一天。说明这条通过这条RRA,我们可以取得一天的数据值。
一天后,又经过一个小时。就会产生第25条,那么如何记录这个第25条数据呢?根据我们这个RRA的定义,它将会替代第一条CDP的位置。我们通过下面这个图来说明:
第二个RRA就很好理解了,每288个PDPs记录为一个CDP,也就是一天的平均值为一个CDP,31个CDP存档,也就是一个月。
假设我们在创建数据文档时,有6个DS的变量声明,那么每个变量会有一个独立的RRA为之记录。同时,一个数据文档中可以有多个RRA。这为我们存取数据提供了方便。
4.实例(检测某核心交换的端口)(create_nic_7609.sh)
/usr/local/rrd/bin/rrdtool create /www/rrd/NIC_7609.rrd -s 300 /
DS:ifInOctets1:COUNTER:600:U:U /
DS:ifInOctets2:COUNTER:600:U:U /
DS:ifInOctets9:COUNTER:600:U:U /
DS:ifInOctets11:COUNTER:600:U:U /
DS:ifInOctets14:COUNTER:600:U:U /
DS:ifInOctets53:COUNTER:600:U:U /
DS:ifOutOctets1:COUNTER:600:U:U /
DS:ifOutOctets2:COUNTER:600:U:U /
DS:ifOutOctets9:COUNTER:600:U:U /
DS:ifOutOctets11:COUNTER:600:U:U /
DS:ifOutOctets14:COUNTER:600:U:U /
DS:ifOutOctets53:COUNTER:600:U:U /
RRA:AVERAGE:0.5:1:4800 /
RRA:AVERAGE:0.5:6:2400 /
RRA:AVERAGE:0.5:24:1200 /
RRA:AVERAGE:0.5:288:600 /
RRA:MAX:0.5:1:4800 /
RRA:MAX:0.5:6:2400 /
RRA:MAX:0.5:24:1200 /
RRA:MAX:0.5:288:600
五、数据的更新(update)
在创建好文档后,我们要用程序定时更新数据文档(
.rrd)然后才能根据数据文档画图。以采集核心交换流量为例,首先我们要抓到各端口的流量,我们可以通过snmp协议来取得数据。
1.在FreeBSD上安装net-snmp
$ cd /usr/ports/net-mgmt/p5-SNMP
$ make install clean
会让你填一些email 操作系统的资料,直接回车就可以了。
测试snmp是否安装成功,在提示符下键入:
$ snmpwalk
No hostname specified.
Usage: snmpwalk [options...] <hostname> {<community>} [<objectID>]
UCD-snmp version: 4.2.6
-h this help message.
-H Display configuration file directives understood.
-V display version number.
-v 1|2c|3 specifies snmp version to use.
SNMP Version 1 or 2c specific
……
出现以上信息表示net-snmp安装成功。
2.snmp的使用
$ snmpwalk -v 2c 核心交换IP地址 设备的community_string
3.数据文档的更新
语法:
rrdtool update filename [--template|-t ds-name[:ds-name]...] N|timestamp:value[:value...] [timestamp:value[:value...] ...]
第一个参数为当前时间值(秒累计),后面把所有的更新的数据,按照DS定义的顺序用冒号格开。
4.用Shell的正则表达式过滤通过snmp取得得信息
我们看到了,通过snmp取得的核心交换的信息是非常之多的,这些信息当然不可能都用到,我们要从中选取我们要的信息。我们使用正则表达式对字符流进行过滤并排列成我们需要的方式。
对于正则表达式,这里不做解释,大家可以通过参考一些书辅助一些例子学习。
我们要取得的是端口的流入和流出的数据,所以我们使用snmp中的两个选项:
ifInOctets和
ifOutOctets 。分别对应的是端口的流入流量和流出流量。
特别地,对于采集100M及其以下的端口流量,这两个参数是完全正确的,但是如果是1000M的端口,就会出现流量和实际值相差甚远。在查过很多资料后发现,原因是这样方式的计数模式,计数字长为32bit,如果采集1000M端口数据会发生数据溢出。我们要使用64bit的字长来计数。所以我们应该选用的参数为:
ifHCInOctets和
ifHCOutOctets。
5.实例(NIC_7609.sh)
综合4和5我们一起来看一个实例,这个实例是配合第四点的实例中的DS定义的更新脚本。
RRD_FILE=/www/rrd/NIC_7609.rrd (.rrd数据文档的位置)
sec=300 #睡眠时间,也就是采集周期
while [ 1 ] #用一个死循环,呵呵
do
rrd_data="" #下面实际用到的是正则表达式的串拼接
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCInOctets |grep ".*ifHCInOctets.1 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCInOctets |grep ".*ifHCInOctets.2 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCInOctets |grep ".*ifHCInOctets.9 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCInOctets |grep ".*ifHCInOctets.11 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCInOctets |grep ".*ifHCInOctets.14 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCInOctets |grep ".*ifHCInOctets.53 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCOutOctets |grep ".*ifHCOutOctets.1 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCOutOctets |grep ".*ifHCOutOctets.2 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCOutOctets |grep ".*ifHCOutOctets.9 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCOutOctets |grep ".*ifHCOutOctets.11 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCOutOctets |grep ".*ifHCOutOctets.14 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' ':'`
rrd_data=$rrd_data`snmpwalk -v 2c IP地址 snmp团体名 ifHCOutOctets |grep ".*ifHCOutOctets.53 = " | sed -e 's/.*: /(.*/)$//1/'| tr '/n' '/0'`
now=`date +%s` #当前时间距离1970的秒数
/usr/local/rrd/bin/rrdtool update $RRD_FILE $now:${rrd_data} #调用更新指令
sh ./NIC_7609_Graph.sh #调用绘图脚本画图,下面一节就介绍
sleep $sec #休息,等待下一个周期的到来
done #循环体结束
六、数据绘图
语法很多 ,不可能一一罗列,有兴趣的可以到官方网站上去查Manual这里提供一些常用的。
常用语法:
rrdtool graph filename 图的文件名
[-s|--start seconds] 绘图起始时间距离1970的秒数
[-t|--title title] 图上显示的标题
[-v|--vertical-label text] Y轴上的说明文字
[-w|--width pixels] [-h|--height pixels] 绘图区域,注意哦,是画图区宽和高
画图的配套参数就介绍这么多,我们要把重点放在图的数据变量的描述和画图具体过程上面。
1.DEF(Define)就是定义一个变量
语法:
DEF:
vname
=
rrd
:
ds-name
:
CF
很容易看出来,这是定义一个虚拟的变量,变量从(
.rrd)数据文件中取得数据源(DS)经过数据合并(CF)后的数据。看到这里,大家应该知道,前面在定义文档中为什么有那么多的参数,其实都是为了绘图做准备的。
①
vname:虚拟变量名,我们自己取的,以后还要用到。
②
rrd:ds-name:CF :数据文件(
.rrd)的全路径 -> 数据源变量 -> 合并方法
举个例子吧:DEF:in_bytes_1=$RRD_PATH:ifInOctets1:AVERAGE
官方解释为:
Define virtual name for a data source. This name can then be used in the functions explained below. The DEF call automatically chooses an
RRA which contains CF consolidated data in a resolution appropriate for the size of the graph to be drawn. Ideally this means that one data point from the
RRA should be represented by one pixel in the graph. If the resolution of the
RRA is higher than the resolution of the graph, the data in the RRA will be further consolidated according to the consolidation function (CF) chosen.
然而,我们觉得光有记录的数据源变量还是不够的,我们希望这些数据源变量可以计算。比如我希望把某两个端口的流量加在一起作为一个变量画图,那么这是我们就需要CDEF
2.CDEF
语法:
CDEF:vname=rpn-expression
先举一个例子,我们从例子中说明问题。我们取得某端口流入流量的字节数,我们希望画在图上的是bit为单位,很明显我们要将字节数乘以8。
例:DEF:in_bytes_1=$RRD_PATH:ifInOctets1:AVERAGE #这句刚刚说过了
CDEF:in_bits_1= 8,in_bytes_1,* 将DEF中定义的in_bytes_1×8放在in_bits_1
其实很好理解,这里唯一困难的就是为什么不写成in_bits_1= in_bytes_1* 8。现在我们回到语法解释rpn(Reverse Polish Notation)逆波兰表达式。语法规定,表达式必须以逆波兰表达式的方式给出。那么什么是逆波兰表达式呢?逆波兰表达式又叫做后缀表达式。这个知识点在数据结构和编译原理这两门恐怖的课程中都有介绍,下面是一些例子:
正常的表达式
|
逆波兰表达式
|
a+b
|
a,b,+
|
a+(b-c)
|
a,b,c,-,+
|
a+(b-c)*d
|
a,d,b,c,-,*,+
|
其实画图似乎用不到这么复杂的表达式,还有不懂的就只有翻看相关的书籍了。
3.画图(最常用的是:线、区域)
从图中可以看出有两种表示流量的方式,流入用绿色的区域(AREA),流出用蓝色的线(LINE)。这就是画图的几个元素。我们还是先看一下语法。
语法:
AREA:
vname
[#
rrggbb
[:
legend
]]
LINE{1|2|3}:
vname
[#
rrggbb
[:
legend
]]
① vname:根据虚拟变量(vname)画图。
② #rrggbb:颜色的16进制表示,用过firework就很熟悉喽
③
legend:该颜色的提示
④ 特别的,画线有粗细之分,所以有LINE1-LINE3
例子:AREA:in_bits_1#00cc00: " Current In"
LINE1:out_bits_1#0000ff: " Current Out "
4.零零碎碎的提示
都希望图下面给点提示,比如最大流量,平均值,等等。那么我们就必须使用这两条指令GPRINT和COMMENT。
语法:
GPRINT:
vname
:
CF
:
format
COMMENT:
text
没有什么难点,有点像C语言的表达式,举两个例子大家就会很好理解的。例:
COMMENT: "Hello World ./c " 以居中的方式显示Hello world
GPRINT:in_bits_1:AVERAGE:"Average Current/:%.2lf%S bps"
显示in_bits_1的值,精确到小数点后面两位。
5.实例(NIC_7609_Graph.sh)
绘图的时候,我们还特别画出了采集端口的日流量、周流量、月流量和年流量。配合Shell脚本的使用,可以达到很好的效果。
image_path=/www/web/nicimages
RRD_PATH=/www/rrd/FJNUNIC_7609.rrd
port="1 2 9 11 14 53" #端口列表
for p in $port #对每个端口的循环
do
DEFin="DEF:in_bytes_$p=$RRD_PATH:ifInOctets$p:AVERAGE"
DEFout="DEF:out_bytes_$p=$RRD_PATH:ifOutOctets$p:AVERAGE"
CDEF="CDEF:in_bits_$p=8,in_bytes_$p,* CDEF:out_bits_$p=8,out_bytes_$p,*"
ddate=`date` #取得当前日期
#以下流入
DRAW_IN="COMMENT:/"Last updated time : $ddate/c/"" #最后更新
DRAW_IN="${DRAW_IN} COMMENT:/"/n/"" #换行
DRAW_IN="${DRAW_IN} AREA:in_bits_$p#00cc00:/" Current In /n/" " #流入流量
DRAW_IN="${DRAW_IN} COMMENT:/" /" GPRINT:in_bits_$p:LAST:/"Last Current/:%.2lf%S bps/"" #流入当前流量文字提示
DRAW_IN="${DRAW_IN}GPRINT:in_bits_$p:AVERAGE:/"Average Current/:%.2lf%S bps/"" #流入平均流量文字提示
DRAW_IN="${DRAW_IN} GPRINT:in_bits_$p:MAX:/"Max Current/:%.2lf%S bps/n/""
#流入最大流量文字提示
#以下流出
DRAW_OUT="LINE1:out_bits_$p#0000ff:/" Current Out/n/" " #流出流量
DRAW_OUT="${DRAW_OUT}COMMENT:/" /" GPRINT:out_bits_$p:LAST:/"Last Current/:%.2lf%S bps/"" #流出当前流量文字提示
DRAW_OUT="${DRAW_OUT}GPRINT:out_bits_$p:AVERAGE:/"Average Current/:%.2lf%S bps/"" #流出平均流量文字提示
DRAW_OUT="${DRAW_OUT} GPRINT:out_bits_$p:MAX:/"Max Current/:%.2lf%S bps/n/"" #流出最大流量文字提示
ttime="d w m y" #时间列表;d日;w周;m月;y年
for t in $ttime #对每个时间循环
do
sec=`date -v-1$t +%s` #绘图起始时间的确定
cmd="/usr/local/rrd/bin/rrdtool graph $image_path/FJNUNIC_7609_IF${p}_${t}.png /
--title '${ttitle}' /
-v ' Bits Per Second' /
-s $sec /
-l 0 -h 150 -w 500 $DEFin $DEFout $CDEF $DRAW_IN $DRAW_OUT
--color CANVAS#ffffff
--color BACK#ffffff /
--color FONT#000000 /
--color MGRID#80C080 /
--color GRID#808020 /
--color FRAME#808080 /
--color ARROW#ff0000 /
--color SHADEA#404040 /
--color SHADEB#404040"
eval $cmd # 执行画图
done #时间循环结束
done #端口循环结束