IPerf——网络测试工具介绍与源码解析(3)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【线程的生成】 生成线程时需要传入一个thread_Settings类型的变量,thread_Settings包含所有线程运行时需要的信息,命令行选项参数解析后所有得到的属性都存储到该类型的变量中,作为线程生成的传入值能够决定当前线程扮演的角色。
【线程的生成】
 
生成线程时需要传入一个thread_Settings类型的变量,thread_Settings包含所有线程运行时需要的信息,命令行选项参数解析后所有得到的属性都存储到该类型的变量中,作为线程生成的传入值能够决定当前线程扮演的角色。
thread_Settings结构中有两个thread_Settings*类型的变量runNow和runNext,runNow不为NULL时表示生成当前Setings所决定的线程之前要先生成包含该指针指向的Settings特征信息的线程,换句话说就要并发运行线程;runNext则表明在当前Settings所决定的线程结束后随即要生成包含该指针指向的Settings特征信息的线程,这中情况分别表现在客户端多并发连接测试和交易测试模式执行时。thread_Settings结构中ThreadMode枚举类型的变量mThreadMode指明线程扮演的角色。
 
  生成线程的入口函数

 

【报告者线程 kMode_Reporter】

IPerf不管是在客户端还是在服务端,都会创建一个报告者线程,该线程是用来输出各种信息到控制台界面,根据其报告的内容可将信息分为五种类型,这些类型都在代码中做了定义标识

复制代码
1 /*
2  * The type field of ReporterData is a bitmask
3  * with one or more of the following
4  */
5 #define    TRANSFER_REPORT      0x00000001
6 #define    SERVER_RELAY_REPORT  0x00000002
7 #define    SETTINGS_REPORT      0x00000004
8 #define    CONNECTION_REPORT    0x00000008
9 #define    MULTIPLE_REPORT      0x00000010
复制代码

 

传输类型:数据传输过程中的数据体现,例如:

[ ID] Interval       Transfer     Bandwidth
[244]  0.0- 1.0 sec   131 MBytes  1.10 Gbits/sec
[244]  1.0- 2.0 sec   281 MBytes  2.36 Gbits/sec
[244]  2.0- 3.0 sec   310 MBytes  2.60 Gbits/sec

服务端返回类型:UDP模式下打印服务端返回的内容,主要为延迟抖动、丢包率的统计信息,例如:

[244] Server Report:
[244]  0.0-10.0 sec  1.25 MBytes  1.05 Mbits/sec   0.000 ms    0/  893 (0%)

设置类型:对于客户端,打印连接的对端地址和连接的端口,对于服务端,打印监听连接的端口等,例如:

------------------------------------------------------------
Client connecting to 127.0.0.1, UDP port 5001
Sending 1470 byte datagrams, IPG target: 11215.21 us (kalman adjust)
UDP buffer size: 64.0 KByte (default)
------------------------------------------------------------

 

------------------------------------------------------------
Server listening on TCP port 5001
TCP window size: 64.0 KByte (default)
------------------------------------------------------------

连接类型:打印连接的信息,例如:

[244] local 127.0.0.1 port 24003 connected with 127.0.0.1 port 5001

多播类型:在同一客户端发生多个连接到服务端时,对于服务端,在一定的打印时间段里,比如上面的1.0- 2.0 sec,程序将识别出为同一客户端的数据量进行累加,做一个总的输出打印,例如:

其中[SUM]开头所打印的信息类型为多播类型

[288]  6.0- 7.0 sec   163 MBytes  1.37 Gbits/sec
[ 40]  6.0- 7.0 sec   164 MBytes  1.37 Gbits/sec
[SUM]  6.0- 7.0 sec   327 MBytes  2.74 Gbits/sec
[288]  7.0- 8.0 sec   164 MBytes  1.38 Gbits/sec
[ 40]  7.0- 8.0 sec   164 MBytes  1.37 Gbits/sec
[SUM]  7.0- 8.0 sec   328 MBytes  2.75 Gbits/sec

那么,对于报告者线程,这是如何进行实现的呢?

IPerf维护了一个节点类型为ReportHeader的全局变量ReportRoot,作为维护报告者首部的根节点,该结构的组成情况是这样的:(这里只列出相关的结构,全面具体的结构体内容请进一步查看源码)

ReportHeader中的ReportData中的有个整型类型的type成员变量,它的值表明了该报告者首部属于那种类型的报告,Reporter.cpp会根据此变量的值进行相应的处理。

复制代码
 1 int reporter_process_report ( ReportHeader *reporthdr ) 
 2 {
 3   if ( (reporthdr->report.type & SETTINGS_REPORT) != 0 ) 
 4     {
 5         //...please read the sourc code for getting more information...
 6   } 
 7     else if ( (reporthdr->report.type & CONNECTION_REPORT) != 0 )
 8     {
 9         //...please read the sourc code for getting more information...
10   } 
11     else if ( (reporthdr->report.type & SERVER_RELAY_REPORT) != 0 ) 
12     {
13         //...please read the sourc code for getting more information...
14   }
15 
16   if ( (reporthdr->report.type & TRANSFER_REPORT) != 0 ) 
17     {
18         //...please read the sourc code for getting more information...
19   }
20     return need_free;
21 }
复制代码

 

程序在开始时会对ReportRoot进行初始化,具体表现在InitReport函数和ReportSettings中,前者创建TRANSFER_REPORT | CONNECTION_REPORT类型的报告者首部并插入到ReportRoot链表中(与下文提到的报告者首部链表指代同一个意思),没错,一个报告者首部可以表示为多种报告类型,而不能说只能是一种报告者类型;对于ReportSetting,它仅生成SETTINGS_REPORT类型的报告者首部并插入到报告者首部链表中,该首部仅在开始时打印了设置信息后就从报告者首部链表中销毁,光荣的结束了它短暂的生命周期。
 
报告者线程会在reporter_spawn函数中循环检测在报告者首部链表的根节点是否为空,非空的情况下调用reporter_process_report函数,该函数递归执行,遍历一次报告者首部链表并在有打印内容的情况下进行打印,其次根据其返回值决定是否需要销毁当前的报告者首部节点。在多并发连接进行的情况下,传输类型的报告者首部节点有多个。
 

报告者线程的绝大部分时间都花在打印传输类型的报告内容。

 

报告者线程的职能还未阐述完,需要结合下面的客户端线程才能更好地解释其是如何将绝大部分时间花费在打印传输类型的报告者首部的。
 
【客户端线程 kMode_Client】
在命令行选项中输入 -c 选项后表明该程序作为客户端运行,作为客户端运行时,首先会走一次client_init函数,在双向测试模式或者交易模式下会添加生成监听者线程的逻辑,如果选项-P在用户输入的命令行选项参数中有体现的话,那么就意味着客户端要进行多并发连接到服务端,那么根据-P选项带进来的线程数添加生成相应数目的客户端线程的逻辑。
 

 

 

在初始化传输报告首部这一步,程序在初始化传输类型的ReportHeader时会申请如下结构的空间大小:

 其中ReportStruct类型共有NUM_REPORT_STRUCTS(#define NUM_REPORT_STRUCTS 700)个,后面它是循环使用的。

 //src/Reporter.c/InitReport

复制代码
 1         reporthdr = (ReportHeader *) malloc( sizeof(ReportHeader) +
 2                             NUM_REPORT_STRUCTS * sizeof(ReportStruct) );
 3         if ( reporthdr != NULL ) 
 4     {
 5             // Only need to make sure the headers are clean
 6             memset( reporthdr, 0, sizeof(ReportHeader));
 7             reporthdr->data = (ReportStruct*)(reporthdr+1);
 8             reporthdr->multireport = agent->multihdr;
 9             data = &reporthdr->report;
10             //Set reporterindex with the last one
11             reporthdr->reporterindex = NUM_REPORT_STRUCTS - 1;
12             ...
13             ...
复制代码

 ReportHeader的data指向第一个ReportStruct结构的地址,agentindex和reporterindex为整型类型,作为data的下标与其结合,data[agentindex]表示当前最新发送包所在的填充位置,data[reporterindex]为报告者线程已报告到控制台的数据包的位置。

客户端线程每次发送数据到服务端后,都会填充一次ReportStrut结构,重要的信息有三项,记录当前发送的数据量大小、包发送出去的时间戳以及包的标识ID,所以可以把ReportStruct看作是Packet,毕竟ReportStructural的成员变量的命名说明其作为一个packet看待会更好,然后会将其填充到data[agentindex]中,并且将angentindex进行加一处理。当填充到ReportStrut数组的尾部时则会回到数组的第一项重新填充,以此方式循环利用,reporterindex永远不能超过agentindex,因为我数据都没填充,残留的是无效的数据,怎么可以进行提前打印呢。

 

来,再说得具体点。

首先,在InitReport函数中,如果选项参数中有使用到有-i选项的话(该选项参数的值存储在thread_settings类型的mInterval变量中),则将该值赋予ReportHeader中ReportData的intervalTime变量,然后将当前时间赋予ReportData的starttime变量(通过gettimeofday),再将startime + intervalTime初始化ReportData中的nexttime,这个值说明下一次将要打印报告的时间戳,具体看代码:

复制代码
1             if ( agent->mInterval != 0.0 )
2             {
3                 struct timeval *interval = &data->intervalTime;
4                 interval->tv_sec = (long) agent->mInterval;
5                 //Equal to Zero Josephus
6                 interval->tv_usec = (long) ((agent->mInterval - interval->tv_sec)
7                                             * rMillion);
8             }
复制代码

//starttime和nexttime的初始化

复制代码
1             else 
2             {
3 
4                 // set start time
5                 gettimeofday( &(reporthdr->report.startTime), NULL );
6             }
7             reporthdr->report.nextTime = reporthdr->report.startTime;
8             TimeAdd( reporthdr->report.nextTime, reporthdr->report.intervalTime );
复制代码

 

然后,在每次客户端线程发完数据后,判断-i选项是否有效,有效的情况下,给当前的包,也就是ReportStruct结构类型的变量填充值,包括发送的数据量大小currLen,获取当前的时间戳,PackID在TCP模式下起的作用只有一个——在发送完毕时添加一个数据量为0,PacketID为-1的包标识发送数据完毕,其余的时候PaketID的值均为0,然后调用ReportPacket函数。

ReportPacket函数的作用是维护agentindex和reportindex的先后关系,将数据包的内容添加到ReportHeader->data[agentindex]中,并将agentindex做加一处理。

 

此时,报告者线程在reporter_spawn中做循环操作,这点在开始的时候也有提到过,循环操作中有调用reporter_process_report函数,所以也可以说reporter_process_report函数一直被报告者线程调用,在该函数中,当处理到运输类型的报告首部时,首先对reporterindex和agentindex在某些特殊情况下进行了处理,确保reporterindex没有“超越”agentindex,然后调用reporter_handle_packet函数,来重点看一下这个函数:

reporter_handle_packet函数一开始就判断当前将要打印(或报告)的包(data[reporthdr->reporterindex])是否是最后一个包(通过PacketID值是否小于0),如果是,则将finished置为1,后面将这个值返回,上层可以通过函数的返回值销毁该运输类型结构体变量,如果不是,则调用reporter_condprintstats函数,但在调用该函数时,将当前可能要打印的包的时间赋予ReportData中的packetTime,注意此时并没有把该包的大小也加到ReportData的TotalLen中,而是等到reporter_condprintstats函数返回时才加上,原因等下说明,来深究一下reporter_condprintstats这个函数:

 reporter_condprintstats函数中,如果传进来的参数force不等于0,在TCP模式下说明数据发送完了,将要打印的是统计的信息,如果force等于0,则会执行循环,循环的条件为:

1     else while ((stats->intervalTime.tv_sec != 0 || stats->intervalTime.tv_usec != 0) &&
2                   TimeDifference( stats->nextTime, stats->packetTime ) < 0 ) 

选项参数-i有使用,体现在隔段时间需要将当前发送信息以打印的方式报告一次,stats是ReportHeader中的ReporterData,其实“罪魁祸首”,起到最大作用的就是ReportData类型的成员变量report,也就是现在的stats,如果nexttime 小于 当前可能要打印的包的时间戳(注意在上层已经将包的时间戳赋予了packetTime),想象一下,本来要nexttime这个时间戳打印报告的,但是现在还没打印的第一个包的时间戳都超过了这个时间,那还不赶紧打印,所以符合条件,开始执行循环体的内容,对ReportData中Transfer_Info类型的变量info进行赋值,并注意保存本次的状态信息并在下次打印时做一系列的相减操作,接着调用reporter_print函数并传入Transfer_info类型的参数值进行控制台输出打印。一般来说,该while循环只执行一次,除非打印的时间间隔太小,也就是-i选项值设的过小,如果想要实现while循环执行多次的效果,可以试试在客户端线程发送数据完毕后紧接着在后面阻塞一段时间。

刚才提到的为什么在reporter_condprintstats函数返回时才加上将要打印的包的大小,因为循环体条件中判断两个时间时使用的是小于符号,注定后面的包大小不宜在该时间段中打印出来。

 如果还不太明白,可以结合下图来理解的:)

 

未完待续...

目录
相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
79 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
65 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
69 0
|
2月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
91 0
|
2天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
2天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
2天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
25天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
53 12
|
21天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。

推荐镜像

更多