IPerf——網絡測試工具介紹與源碼解析(3)


【線程的生成】
 
生成線程時需要傳入一個thread_Settings類型的變量,thread_Settings包含所有線程運行時需要的信息,命令行選項參數解析后所有得到的屬性都存儲到該類型的變量中,作為線程生成的傳入值能夠決定當前線程扮演的角色。
thread_Settings結構中有兩個thread_Settings*類型的變量runNow和runNext,runNow不為NULL時表示生成當前Setings所決定的線程之前要先生成包含該指針指向的Settings特征信息的線程,換句話說就要並發運行線程;runNext則表明在當前Settings所決定的線程結束后隨即要生成包含該指針指向的Settings特征信息的線程,這中情況分別表現在客戶端多並發連接測試和交易測試模式執行時。thread_Settings結構中ThreadMode枚舉類型的變量mThreadMode指明線程扮演的角色。
 
 1 DWORD WINAPI thread_run_wrapper( void* paramPtr ) 
 2 {
 3     struct thread_Settings* thread = (struct thread_Settings*) paramPtr;
 4     switch ( thread->mThreadMode ) 
 5     {
 6         case kMode_Server:
 7             {
 8                 server_spawn( thread );
 9             } break;
10         case kMode_Client:
11             {
12                 client_spawn( thread );
13             } break;
14         case kMode_Reporter:
15             {
16                 reporter_spawn( thread );
17             } break;
18         case kMode_Listener:
19             {
20             } break;
21         default:
22             {
23                 FAIL(1, "Unknown Thread Type!\n", thread);
24             } break;
25     }
26 
27     if ( thread->runNext != NULL ) 
28         {
29         thread_start( thread->runNext );
30     }
31     Settings_Destroy( thread );
32 
33     return 0;
34 } // end run_wrapper
生成線程的入口函數

 

【報告者線程 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函數返回時才加上將要打印的包的大小,因為循環體條件中判斷兩個時間時使用的是小於符號,注定后面的包大小不宜在該時間段中打印出來。

 如果還不太明白,可以結合下圖來理解的:)

 

未完待續...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM