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

 

傳輸類型:數據傳輸過程當中的數據體現,例如:app

[ 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/secide

服務端返回類型:UDP模式下打印服務端返回的內容,主要爲延遲抖動、丟包率的統計信息,例如:函數

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

設置類型:對於客戶端,打印鏈接的對端地址和鏈接的端口,對於服務端,打印監聽鏈接的端口等,例如:spa

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

 若是還不太明白,能夠結合下圖來理解的:)

 

未完待續...

相關文章
相關標籤/搜索