標籤:
【線程的產生】 產生線程時需要傳入一個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 bitmask3 * with one or more of the following4 */5 #define TRANSFER_REPORT 0x000000016 #define SERVER_RELAY_REPORT 0x000000027 #define SETTINGS_REPORT 0x000000048 #define CONNECTION_REPORT 0x000000089 #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 one11 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 Josephus6 interval->tv_usec = (long) ((agent->mInterval - interval->tv_sec)7 * rMillion);8 }
//starttime和nexttime的初始化
1 else 2 {3 4 // set start time5 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函數返回時才加上將要列印的包的大小,因為迴圈體條件中判斷兩個時間時使用的是小於符號,註定後面的包大小不宜在該時間段中列印出來。
如果還不太明白,可以結合來理解的:)
未完待續...
IPerf——網路測試載入器介紹與源碼解析(3)