在英語中,“Siege”意為圍攻、包圍。同時Siege也是一款使用純C語言編寫的開源WEB壓測工具,適合在GNU/Linux上運行,並且具有較強的可移植性。之所以說它是多線程編程的最佳實例,主要原因是Siege的實現原理中大量運用了多線程的各種概念。Siege代碼中用到了互斥鎖、條件變量、線程池、線程信號等很多經典多線程操作,因此對於學習多線程編程也大有裨益。最近花了一些時間學習到了Siege的源代碼,本文將介紹一下Siege壓測工具的內部原理,主要供系統測試同學、以及學習多線程編程的同學們參考。
一、工具背景
Siege是一名叫做Jeff Fulmer的伙計發起的開源項目,他的主頁是:http://www.joedog.org/ 。從頁面上看,Jeff Fulmer自從1999年起便開始“serving the Internets”,也算是一名老程序員了。Siege可謂是作者最傑出的作品。這款壓測工具的名稱“圍攻”也比較生動形象展示了工具用途,即“圍攻web服務器”。
Siege使用多線程實現,支持隨機訪問多個URL,可以通過控制並發數、總請求數(or壓測時間)來實現對web服務的壓測。Siege支持http,https,ftp三種請求方式,支持GET和POST方法,壓測方式為同步壓測,全部源代碼總共13000行。功能還是非常全面的,很適合web開發在服務器開發完成後進行自測時使用。
二、工具使用
該工具主要在Linux環境下使用,下載鏈接為:http://download.joedog.org/siege/ 。安裝方式和正常的linux環境軟件安裝步驟大致相同,先解壓縮,再 config->make->make install。
$ tar –xzvf siege-3.0.8.tar.gz $ cd siege-3.0.8 $ ./config $ make $ make install
在安裝中需要注意的是make和make install可能會要求管理員權限,所以可能需要在make 和make install前面加上sudo。
使用方法如下:
siege [options]
或者 siege [options] URL
其中options
可選項有:
-V --version 打印版本信息 -h --help 打印幫助信息 -v --verbose 在測試過程中輸出更多的通知信息 -C --config 打印當前的配置信息(siege有一個名為.siegerc的配置文件) -q --quite 此選項會覆蓋掉--verbose,是安靜模式,在測試中減少信息輸出 -g --get 顯示http頭信息,適用於debug -c --concurrent 最為常用的參數,每次測試必設置,並發數量,例 -c10代表10個並發 -i --internet 隨機點擊URL,在同時測試多個URL時可以使用,模擬用戶隨機訪問的情形 -b --benchmark 每個請求之間沒有延時,也是很常用的設置 -t --time 非常常用的參數,設置測試的時間,默認以分鐘為單位,其他單位要自己設置,例如 -t10s,測試持續10秒 -r --reps 非常常用的參數,指定了測試幾個回合結束,本參數和-t都可用來設置測試結束條件。 -f --file 指定一個存放URL鏈接的文件。siege支持隨機訪問多個url,因此這些url鏈接在文件中提供,較為常用。 -l --log 指定log文件,如果沒有指定的話siege也有默認文件保存位置,文件名siege.log -d --delay 指定時間延遲,在每個請求發出後,再隨機延遲一段時間再發下一個 -H --header 指定http請求頭部的一些內容 -A --user-agent 指定http請求中user-agent字段內容 -T --content-type 指定http請求中的content-type字段內容
上面列了一大坨參數,其實還沒有列全,有一些更少用的沒有列出來。實際上,如果只是簡單使用的話,大部分都不需要搞清楚。上文中有幾個常用的功能選項已經注明(-b
, -c
, -t
, -r
, -f
),掌握這幾個基本就夠用了。我們先來簡單使用一下,有一個更清楚的認識。
horstxu@horstxu-Lenovo-G400:~/Downloads/siege-3.0.8$ siege http://www.[某個網站].com -c10 -t5s -b ** SIEGE 3.0.8 ** Preparing 10 concurrent users for battle. The server is now under siege... HTTP/1.1 200 0.14 secs: 1917 bytes ==> GET / HTTP/1.1 200 0.15 secs: 1917 bytes ==> GET / …………………… HTTP/1.1 200 0.16 secs: 1917 bytes ==> GET / Lifting the server siege... done. Transactions: 325 hits Availability: 100.00 % Elapsed time: 4.89 secs Data transferred: 0.59 MB Response time: 0.15 secs Transaction rate: 66.46 trans/sec Throughput: 0.12 MB/sec Concurrency: 9.85 Successful transactions: 325 Failed transactions: 0 Longest transaction: 0.21 Shortest transaction: 0.11
上面省略號省略了一些冗余的輸出,並且我們屏蔽網站域名免得打廣告。在上面的測試中,我們設置了10個並發用戶,測試5秒時間,並且每個請求之間沒有時延,也就是收到回復後馬上發出下一個。測試的結果是,4.89秒內完成了325次請求,共傳輸0.59MB的數據,平均響應時間0.15秒,平均每秒66.46次請求,拓撲量0.12MB每秒,並發數平均9.85。統計的數據還算比較全面。
三、原理介紹
先簡單畫一下程序的流程圖,如下圖所示
如果並發用戶數為n,那麼就會相應創建n個壓測線程,每個線程模擬1個用戶。除了壓測線程之外,主函數會額外生成2個線程,我們暫且稱之為計時線程和控制線程。計時線程用於等待一開始我們設定的壓測時間,到時間後通過線程信號通知控制線程。隨後控制線程通過改變與壓測線程共享的壓測停止標志位,並發送終止信號來實現壓測線程的停止。每個壓測線程都會從結構體CREW中讀取壓測任務,這些壓測任務由主函數添加。每個線程的測試數據均會輸出到client結構體數組中,最後由主函數統一收集結果,並打印在屏幕上。
這一過程當中涉及的線程操作有條件變量,用於等待CREW中有壓測任務到來,另外在計時線程中也用到了條件變量進行計時操作;互斥鎖,用於改變CREW結構體成員的值時加鎖保護數據;線程信號,用於線程間的相互通知;信號屏蔽字,用於將到來的異步信號用同步的方法去處理。源碼中一大堆以pthread開頭的函數操作,如果不清楚細節的話可以翻閱一下《UNIX環境高級編程》這本編程聖經來查閱一下。接下來我們進行更詳細一些的代碼分析。
四、源碼分析
CREW
與client
兩個結構體CREW
是用來統一管理所有壓測線程的結構體,它在主函數中被聲明,因此可以被所有線程共享。對其中成員變量的改動也需要加鎖後進行。CREW
結構體如下:
struct CREW_T //用於管理所有壓測線程的結構體 { int size; //目標並發數目,即壓測線程個數 int maxsize; //最大並發數目,即壓測線程個數 int cursize; //目前的可用並發數,壓測中時這個數字隨壓測線程實時變化 int total; //實際啟動的並發數 WORK *head; //壓測任務鏈表頭部 WORK *tail; //壓測任務鏈表尾部 BOOLEAN block; //當已經達到最大並發時,則不准再添加新的壓測線程 BOOLEAN closed; //壓測線程是否已經關閉 BOOLEAN shutdown; //壓測線程是否應該停止了 pthread_t *threads; //長度為size的數組,存儲線程號 pthread_mutex_t lock; //修改本結構體都要先加鎖 pthread_cond_t not_empty; //用於表示cursize不為0的條件 pthread_cond_t not_full; //用於表示cursize不等於maxsize的條件 pthread_cond_t empty; //用於表示cursize等於0的條件 };
每個壓測線程都會維護屬於自己的一份client
,他們共同構成一個長度為n的數組。該結構體用於存儲屬於壓測線程的相關信息,例如請求的響應時間,請求次數,數據流量等。這些統計信息最終將會反映給主進程做匯總輸出。
typedef struct { int id; //client編號,對於n個線程編號分別從0至n-1 unsigned long hits; //共完成幾次transaction,每完成一次請求加1 unsigned long bytes; //收到的數據總量 unsigned int code; //返回碼是小於400的,或者等於401,等於407,則該計數加1 unsigned int fail; //失敗計數,只要返回碼大於等於400,且不是401也不是407,則該計數加1 unsigned int ok200; //返回碼是200的數量,200為成功請求 ARRAY urls; //要訪問的URL列表 struct { DCHLG *wchlg; DCRED *wcred; int www; DCHLG *pchlg; DCRED *pcred; int proxy; struct { int www; int proxy; } bids; struct { TYPE www; TYPE proxy; } type; } auth; //本結構體用於設置代理服務器信息以及鑒權信息 int status; //連接狀態信息,包括未連接,正在連接,待讀取等 float time; //統計請求花費的總時長 unsigned int rand_r_SEED; //隨機數種子,用於隨機訪問URL的場景 float himark; //最慢一次請求花費的時間 float lomark; //最快一次請求花費的時間 } CLIENT;
寫到這裡,其實本程序代碼為什麼有13000行之多已經可以看到原因了。作者對於很多模塊都進行了封裝,比如C語言沒有的BOOLEAN類型,數組操作ARRAY類型,壓測任務鏈表操作WORK類型,已經與C++中的class有些類似。我們可以舉個簡單的例子,比如WORK
類型是這麼定義的:
typedef struct work { void (*routine)(); void *arg; struct work *next; } WORK;
這裡面的routine
是一個函數指針,而arg
是要傳給前面函數的參數。整個壓測任務由一個單向鏈表來存儲在CREW
中。程序中這樣的例子還有很多,就不再贅述。接下來我們關注一下計時線程、控制線程、壓測線程的核心代碼。
計時線程在到達一定時間之後,會向控制線程發送SIGTERM信號,通知控制線程停止壓測。該函數並不算復雜,下面是核心代碼,我們略去了一些不必要的代碼,只展示出了最重要的部分:
void siege_timer(pthread_t handler) //handler是控制線程的id { int err; time_t now; struct timespec timeout; pthread_mutex_t timer_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t timer_cond = PTHREAD_COND_INITIALIZER; //專門用來計時的條件變量 if (time(&now) < 0) { NOTIFY(FATAL, "unable to set the siege timer!"); } timeout.tv_sec=now + my.secs; //設置超時時間,my.secs就是我們設置的壓測時間,以秒為單位 timeout.tv_nsec=0; pthread_mutex_lock(&timer_mutex); for (;;) { err = pthread_cond_timedwait( &timer_cond, &timer_mutex, &timeout);//使用條件變量進行計時操作 if (err == ETIMEDOUT) { /* timed out */ pthread_kill(handler, SIGTERM); //向handler線程發送sigterm信號 break; } else { continue; } } pthread_mutex_unlock(&timer_mutex); return; }
這段代碼還是比較容易理解的,條件變量在到時之前根本不會被激活,基本上是因為計時到了而返回,這也是pthread_cond_timedwait
的作用。為了使用條件變量,外面又包了一層互斥鎖timer_mutex
,雖然根本不會有其他線程來搶這把鎖。到時間後,通過pthread_kill
來向其他線程發送信號。
控制線程其實只做一件事情,即等待計時線程發送終止信號,收到信號後調用相關函數取消正在執行的壓測線程。這次同樣略去一些代碼,只看最核心的控制線程部分。相關代碼如下:
void sig_handler(CREW crew) { int gotsig = 0; sigset_t sigs; sigemptyset(&sigs); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGTERM); sigprocmask(SIG_BLOCK, &sigs, NULL); //設置信號屏蔽字,在sigwait之前必須先屏蔽信號 /** * Now wait around for something to happen ... */ sigwait(&sigs, &gotsig);//阻塞等待線程信號,用於響應計時線程pthread_kill發來的信號 fprintf(stderr, "\nLifting the server siege..."); crew_cancel(crew); //取消CREW中的所有任務,即讓壓測線程停止下來 /** * The signal consistently arrives early, * so we artificially extend the life of * the siege to make up the discrepancy. */ pthread_usleep_np(501125); //人為使線程睡眠一小會,上面英文為原作者的注釋 pthread_exit(NULL); }
計時和控制線程還是比較容易理解的,代碼結構也相對較為簡單,接下來就瞧一下最為繁瑣的壓測線程。主函數將會通過for循環來創建n個壓測線程,每個線程執行如下函數(同樣略去了非關鍵代碼):
private void *crew_thread(void *crew)//壓測線程,共有size個,取決於命令行-c後面的數字 { WORK *workptr; //壓測函數結構體的指針,真正的壓測邏輯都在這裡的函數中實現 CREW this = (CREW)crew; //這裡的結構體CREW正是前文4.1節中提到的CREW,用於管理所有壓測線程 while(TRUE){//這裡是死循環,壓測一直在循環執行中,除非調用pthread_exit退出 pthread_mutex_lock(&(this->lock)); while((this->cursize == 0) && (!this->shutdown)){//如果目前可用並發數cursize是空的,則等待 pthread_cond_wait(&(this->not_empty), &(this->lock)); //一開始創建的size個壓測線程都會卡在這裡 } if(this->shutdown == TRUE){ //線程停止,則釋放鎖,退出,這裡是唯一可以停止壓測的地方 pthread_mutex_unlock(&(this->lock)); pthread_exit(NULL); } workptr = this->head; //取出第一個節點上的壓測程序 this->cursize--; //可用並發數減一 if(this->cursize == 0){ //更新CREW中壓測任務鏈表的值 this->head = this->tail = NULL; }else{ this->head = workptr->next; } if((this->block) && (this->cursize == (this->maxsize - 1))){ pthread_cond_broadcast(&(this->not_full)); } if(this->cursize == 0){ //現在並發量如果為0,喚醒empty condition pthread_cond_signal(&(this->empty)); } pthread_mutex_unlock(&(this->lock)); (*(workptr->routine))(workptr->arg);//這裡才真正在執行壓測函數 xfree(workptr); } return(NULL); }
細心觀察一下即可發現,這裡面都是各種加鎖解鎖的操作,在修改CREW
前後需要拿到互斥鎖才可以進行。一直到很靠後面一句(*(workptr->routine))(workptr->arg)
才真正開始執行壓測函數。並且從中還可以知道,只有把CREW
的shutdown
字段改為true
才可以使壓測線程停下來。那麼接下來觀察一下workptr->routine
的內容。
void *start_routine(CLIENT *client)//主要的壓測函數,每個線程通過此函數進行壓測 { int ret; //function return value CONN *C = NULL; // connection data (sock.h) int type, state; C = xcalloc(sizeof(CONN), 1); C->sock = -1; pthread_cleanup_push((void*)clean_up, C); //設置線程清理函數,在線程退出時會被調用 pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, &type);//修改取消類型為異步取消 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &state);//設置線程取消狀態為可以取消 for (……) { URL tmp = array_get(client->urls);//選擇一個url if (tmp != NULL && url_get_hostname(tmp) != NULL) { if ((ret = __request(C, tmp, client))==FALSE) { //訪問該url __increment_failures(); } } if (my.failures > 0 && my.failed >= my.failures) { break; } } /** * every cleanup must have a pop */ pthread_cleanup_pop(0); //該設置用於取消線程清理函數,pop與push經常成對使用 if (C->sock >= 0){ C->connection.reuse = 0; socket_close(C); } xfree(C); C = NULL; return(NULL); }
上面就是workptr->routine
具體鎖執行的函數。需要注意的是源代碼中這個函數比較長,我們簡略了一大部分,只列出來了少部分主要代碼。實際上,start_routine
依然沒有列出來最細節的部分,比如每個壓測線程中必須要執行的構建socket,連接服務器connect,寫入請求write,收到請求read等過程。這些過程都在上面代碼中出現的__request
函數中。列出上文這些內容主要想講述幾個pthread開頭的函數在壓測中的作用。
pthread_cleanup_push
與pthread_cleanup_pop
用於設置線程清理函數,也就是當線程收到cancel信號退出前會調用的函數。有點類似於常見的try-catch-finally組合中finally的功能。push用於添加線程清理函數,pop用於刪除線程清理函數。需要注意的是線程清理函數使用棧來存儲,可以設置多個,push和pop類似棧的操作,後來的先出去。pop一次只會刪除最晚的一個線程清理函數。
pthread_setcanceltype
表示當函數收到取消(pthread_cancel
)的信號,應該采取何種取消策略。PTHREAD_CANCEL_ASYNCHRONOUS
表示異步取消,即任意時間收到取消信號,線程都可以終止;另外還有一種參數為PTHREAD_CANCEL_DEFERRED
,表示推遲取消,即收到取消信號後一直到線程遇到某取消點才會取消。至於取消點的設置,POSIX中默認了很多系統調用出現時都會出現取消點。這些函數列表大家可以自行去網上搜一下,就不再羅列了。
pthread_setcancelstate
用於設置線程取消狀態,可以是PTHREAD_CANCEL_ENABLE
或PTHREAD_CANCEL_DISABLE
,分別表示可以取消或不可以被取消。如果設為不可以被取消,不管前面的setcanceltype怎麼設置,碰到取消信號時線程都會將該信號掛起並無視它。
在__request
函數中,由於需要區分ftp、http、https請求,需要判斷是否長連接,還可能需要設置cookie,區別POST,GET等不同請求方式,這導致該函數又調用一大堆其他函數,層層深挖下去都列出來可能就是一部長篇小說的長度了。我們就不再深究,更深層的內容其實不難理解。我們在最後來瞧一眼主函數如何將以上這些內容串聯起來。
main函數是將所有壓測流程串聯起來的調度程序。在閱讀源碼的時候,從main函數入手應該是比較明智的選擇,這樣也比較容易看出來整個程序的組織。Siege中的main函數也有300余行代碼,這些代碼並沒有太難理解的地方,我們使用注釋+省略號+偽代碼的方式大概浏覽一下main函數如何將以上過程進行串聯。如下:
int main(int argc, char *argv[]) { /*設置信號屏蔽字*/ sigemptyset(&sigs); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGALRM); sigaddset(&sigs, SIGTERM); sigprocmask(SIG_BLOCK, &sigs, NULL); /*讀取命令行參數、配置文件並解析*/ …… /*用for循環創建壓測線程*/ crew = new_crew(my.cusers, my.cusers, FALSE); /*設置URL相關參數,將要壓測的連接存入內存中*/ …… /*創建控制線程*/ pthread_create(&cease, NULL, (void*)sig_handler, (void*)crew); /*創建計時線程,注意第三個參數和創建控制線程的第一個參數都是cease*/ pthread_create(&timer, NULL, (void*)siege_timer, (void*)cease); for (x = 0; x < my.cusers && crew_get_shutdown(crew) != TRUE; x++) { /*向CREW中添加壓測任務*/ /*也就是設置CREW中WORK鏈表的值*/ /*設置完畢後通過pthread_cond_broadcast解除被條件變量阻塞的壓測線程*/ …… crew_add(crew, (void*)start_routine, &(client[x])); …… } /*等待所有壓測線程結束*/ crew_join(crew, TRUE, &statusp); /*從client數組中讀取壓測數據,匯總到DATA結構體中*/ …… /*將匯總的DATA結構體內容輸出*/ …… /*釋放資源,退出*/ }
源碼暫且看到這裡,一共13000行代碼,如果每一行都搞明白可能會累死。我們最重要的是了解到最核心的內容,清楚整個程序的設計原理即可。Siege除了幫助大家認識一種新的壓測工具實現方法以外,其多線程的各種操作也是讓人大開眼界的。
Siege是由多線程實現的同步壓測工具,它實現的是模擬n個用戶不停地訪問某個URL的場景。由於多線程開銷會比多進程小一些,因此該壓測工具比多進程的壓測工具在系統開銷上會好很多。程序提供了到時停止(到一定時間停止壓測)和到量停止(訪問一定次數後停止壓測)兩種壓測方法,支持同時壓測多個URL,也能夠隨機選取URL進行壓測。支持ftp、http、https,可以發送GET、POST、HEAD等多種請求,可以設置鑒權、cookies。並且程序中特意增加了許多解決不同平台上兼容性的代碼。已經是非常完善的一個工具了,並且到目前位置,Siege的版本依然在更新中。
不過,Siege對於壓力控制並不夠精確,只能粗略地根據並發用戶數去控制一下壓力大小。考慮這樣一種場景,我希望每秒鐘向服務器發送1000個請求,並且第0至1ms發一個,第1至2ms發一個,第2至3ms發一個,……,這樣精度的控制Siege是無法達到的。當然,對於同步壓測程序來說,這樣的精度比較難以實現。另外,Siege的時間控制並不精確,比如在本文中使用Siege的章節可以看到,我想要測試5s,但是實際輸出的測試時間為4.89s。Siege的計時方式是通過times函數取得壓測經歷的的系統時鐘數,並通過sysconf(_SC_CLK_TCK)
取得系統每秒時鐘數,兩者相除得來。另外一個小的缺點是,由於使用多線程實現,一個進程可以開啟的線程數量本身是有限的,並且線程過多的情況下CPU在線程間切換也是一筆不小的開銷,十分影響效率。因此Siege的使用過程中還要注意開啟的並發用戶數不能太多。
最後的最後還要展示一下Siege的源代碼文件,13000行代碼是由以上這一大坨源文件構成,乍一看上去還小吃了一驚,一個小工具寫了如此復雜的代碼。其實從文件名可以看出來每個文件都有很強的封裝思想,如果利用C++來寫,一定會比目前的純C清晰很多。不過作為一款linux系統上運行的工具,可能作者認為純C語言一定是linux編程的首選吧。
考生的學歷必須符合下列條件之一:
(1)國家承認學歷的應屆本科畢業生;
(2)具有國家承認的大學本科畢業學歷的人員;
(3)獲得國家承認的大專畢業學歷後經兩年或兩年以上(從大專畢業到錄取為碩士生當年9月1日,下同),達到與大學本科畢業生同等學力,且符合招生單位根據本單位的培養目標對考生提出的具體業務要求的人員;
國家承認學歷的本科結業生和成人高校應屆本科畢業生,按本科畢業同等學力身份報考;
(4)已獲碩士學位或博士學位的人員,可以再次報考碩士生,但只能報考委托培養或自籌經費的碩士生;
C#語法不是很熟,大概意思你應該也可以看懂。。
using System.Thread;
public class test
{
public void thread1()
{
while(true)
{
Console.print("Thread1");//控制台輸出語句 不太記得了
}
}
public void thread2()
{
while(true)
{
Console.print("Thread2");//控制台輸出語句 不太記得了
}
}
public static void main(String s[])
{
Thread thr1=new Thread(Address of thread1); //指定thr1線程的線程體是thread1函數
Thread thr2=new Thread(Address of thread2);//同理
thr1.start(); //線程一開始運作
thr2.start(); //同理
}
}
補充:
如果要添加到FORM裡面就在FORM裡面放兩個文本框,然後在裡面添加文本就好了。