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

【线程的生成】
 
生成线程时需要传入一个thread_Settings类型的变量,thread_Settings包含所有线程运行时需要的信息,命令行选项参数解析后所有得到的属性都存储到该类型的变量中,作为线程生成的传入值能够决定当前线程扮演的角色。
thread_Settings结构中有两个thread_Settings*类型的变量runNow和runNext,runNow不为NULL时表示生成当前Setings所决定的线程之前要先生成包含该指针指向的Settings特征信息的线程,换句话说就要并发运行线程;runNext则表明在当前Settings所决定的线程结束后随即要生成包含该指针指向的Settings特征信息的线程,这中情况分别表现在客户端多并发连接测试和交易测试模式执行时。thread_Settings结构中ThreadMode枚举类型的变量mThreadMode指明线程扮演的角色。
 
 DWORD WINAPI thread_run_wrapper( void* paramPtr )
{
struct thread_Settings* thread = (struct thread_Settings*) paramPtr;
switch ( thread->mThreadMode )
{
case kMode_Server:
{
server_spawn( thread );
} break;
case kMode_Client:
{
client_spawn( thread );
} break;
case kMode_Reporter:
{
reporter_spawn( thread );
} break;
case kMode_Listener:
{
} break;
default:
{
FAIL(, "Unknown Thread Type!\n", thread);
} break;
} if ( thread->runNext != NULL )
{
thread_start( thread->runNext );
}
Settings_Destroy( thread ); return ;
} // end run_wrapper

生成线程的入口函数

【报告者线程 kMode_Reporter】

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

 /*
* The type field of ReporterData is a bitmask
* with one or more of the following
*/
#define TRANSFER_REPORT 0x00000001
#define SERVER_RELAY_REPORT 0x00000002
#define SETTINGS_REPORT 0x00000004
#define CONNECTION_REPORT 0x00000008
#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,作为维护报告者首部的根节点,该结构的组成情况是这样的:(这里只列出相关的结构,全面具体的结构体内容请进一步查看源码)

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

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

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

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

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

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

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

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

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

 //src/Reporter.c/InitReport

         reporthdr = (ReportHeader *) malloc( sizeof(ReportHeader) +
NUM_REPORT_STRUCTS * sizeof(ReportStruct) );
if ( reporthdr != NULL )
{
// Only need to make sure the headers are clean
memset( reporthdr, , sizeof(ReportHeader));
reporthdr->data = (ReportStruct*)(reporthdr+);
reporthdr->multireport = agent->multihdr;
data = &reporthdr->report;
//Set reporterindex with the last one
reporthdr->reporterindex = NUM_REPORT_STRUCTS - ;
...
...

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,这个值说明下一次将要打印报告的时间戳,具体看代码:

             if ( agent->mInterval != 0.0 )
{
struct timeval *interval = &data->intervalTime;
interval->tv_sec = (long) agent->mInterval;
//Equal to Zero Josephus
interval->tv_usec = (long) ((agent->mInterval - interval->tv_sec)
* rMillion);
}

//starttime和nexttime的初始化

             else
{ // set start time
gettimeofday( &(reporthdr->report.startTime), NULL );
}
reporthdr->report.nextTime = reporthdr->report.startTime;
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,则会执行循环,循环的条件为:

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

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

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

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

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

未完待续...

上一篇:MAC帧和IP数据报


下一篇:[转]Git详解之四 服务器上的Git