Mongoose源碼剖析:mongoose的工作模型
引言
我看一個項目的時候,比較喜歡首先看它的架構和設計。因為這樣在研讀源碼的時候,有一個指導作用,不會迷失于具體細節,并能夠引導我如何去將點串成線,將線串成面。而且一個軟件怎么樣,很大程度上取決于它采用的架構。
本文主要介紹Mongoose的工作模型,及根據這個模型將代碼大致串起來,找出主線。內容框架如下:
- 1、線程模型
- 2、從程序入口著手
- 3、Mongoose的生命旅程
1、線程模型
Mongoose采用了一個自適應的線程池的模型。有一個主線程(master thread)用于打開配置端口和等待連接的到了。一旦新的連接到來,主線程將衍生一個新的線程去服務該連接。當衍生的線程處理完連接的請求之后,它會保持一段時間的空閑(可以通過配置選項-idle_time 控制空閑時間),在此期間主線程可能會傳遞另一個連接給它,讓它服務。
因此,每個連接都是在自己的線程中執行,且線程數量隨著web服務器的負載而變化。然而,最大的活躍線程數由-max_thread 控制。如果一旦總的線程數達到了這個閾值,當新的連接到來時,主線程將等到有線程空閑時在分配線程服務新到來的連接。以此同時,建立了TCP監聽隊列,即當沒有線程空閑時到來的新連接會被置入該隊列,當有線程空閑了會從隊列中取出連接并服務。如果沒有線程變空閑,而TCP隊列又滿了,web服務器將拒絕新到來的連接請求。
上面所述的過程大致如下所示:
圖1、線程模型
2、從程序入口著手
在《Mongoose源碼剖析:Introduction and Installation》中,我們簡單分析了Makefile文件知道生成的mongoose執行文件的入口肯定在main.c中(如果將Mongoose嵌入到你的應用程序中,就由你來決定入口了!)。在典型的main函數入口中,我們可以看到下面的流程:
main(){ 啟動mongoose及設置相關參數(或使用默認的); 聲明幾個信號的處理函數: #ifndef _WIN32 (void) signal(SIGCHLD, signal_handler); #endif /* _WIN32 */ (void) signal(SIGTERM, signal_handler); (void) signal(SIGINT, signal_handler); ctx=mg_start(); process_command_line_arguments(ctx, argv); 進入死循環直到檢測到程序結束標記while(exit_flag==0); mg_stop(); } |
上面即是main函數中的主流程。需要注意的是調用mg_start()之后返回一個mg_context結構體的實例,這個實例將會在整個連接請求中用到,而且如果你在啟動mongoose中設置了參數選項,在下面的process_command_line_arguments()函數中還會對ctx進行修改。從這里我們也知道了,mongoose程序的核心入口時mg_start(),最后終結于mg_stop()。
3、Mongoose的生命旅程
通過上面的分析我們知道Mongoose起始于mg_stop(),終結于mg_stop()。下面我們就從生命之初到生命終結之間的“故事”。說明:在這里我們不會去過于追究細節,只是串線式的把Mongoose的生命流程串起來,哪些細節或許后續的文章來解釋,或者留給讀者你去做了!
在mg_start()主要是做一些初始化的工作,最后才會正式進入工作服務于client。這里的初始化工作就好比一個人的出生需要十月懷胎,為誕生積蓄能量,要從受精卵長成一個完整的人。在準備工作完成之后,mg_start()會啟動一個主線程master_thread,它用于監聽所有的client連接請求。
啟動一個主線程即啟動了一個web server,在主線程中首先會將該server監聽的地址(socket)加入到監聽集合中去。然后一直監聽該端口,只要有client的連接請求到來,它會調用accept_new_connection()去處理連接請求。
接下來,我們關注的是accept_new_connection()是如何去處理連接請求的。首先它會進行一些預判工作,決定是否允許該連接。如果允許,則調用put_socket()并將處理工作轉交給它,所謂權力下放。
在put_socket()首先也會進行一些預判工作——判斷mg_context結構體的成員變量queue隊列是否已滿,如果滿了就等待直到queue有位置容納請求。還有一點要說明的是:由于有可能多個client請求同時到達,對queue進行操作,所以在put_socket()中一開始就設置(void) pthread_mutex_lock(&ctx->thr_mutex);而且請求是通過調節變量來控制等待queue是否有位置容納請求(void) pthread_cond_wait(&ctx->full_cond, &ctx->thr_mutex)。說了這么多準備工作,現在該正式進入工作了。至此,如果沒有空閑進程且進程數量沒有達到最大閾值,就會啟動一個新的工作進程worker_thread去處理client的請求。之后就是釋放信號量等資源,讓其它client請求也能夠請求到資源工作,如啟動了一個工作進程去處理client請求,這時queue就空出一個位置了,它會調用pthread_cond_signal(&ctx->empty_cond)讓等待的client請求知道queue中有位置了。最后就是釋放put_socket()中一開始設置的鎖,(void) pthread_mutex_unlock(&ctx->thr_mutex)。
到了這里,client的請求已經被分配打一個工作線程中去了。而且不同的client請求處理運行在不同的工作線程中,能夠互不干擾。在worker_thread中,首先與client建立連接,只有連接上了才能為client服務。連接建立之后調用process_new_connection()去處理請求。處理完之后返回關閉連接,并通過信號機制告訴主線程,我的做工做完了。
在process_new_connection()中處理工作:首先解析請求parse_http_request(),知道請求的內容;接著就是進入Mongoose處理client請求的真正核心工作了analyze_request()。這里就不詳細介紹parse_http_request()、analyze_request()是如何去解析、驗證、提供具體服務的,否則就陷入了細節出不來了,這里主要是介紹Mongoose的生命之旅的主線。
下面用圖形來形象描述一下Mongoose的生命之旅,說明:該圖形并不是一個精確的邏輯關系,圖中的箭頭方向只是描述了程序的大概流程,并都是上級調用下級的關系,如并不是parse_http_request()調用analyze_request()等,而實際上它們都是在process_new_connection()被調用。
圖2、Mongoose的大概生命主線
4、總結
至此,算是介紹完了Mongoose的一個完整的工作模型了,你可以安裝此主線去進行code review。只有你腦海里有這樣一個模型,你就不會在研讀代碼是迷失了。
當然Mongoose提供的很多api,這里都沒有介紹到,因為它不是本文的重點。我希望此能夠帶后來者步入Mongoose源碼研讀的大門,給后來者節省徘徊在門外停滯不前的時間。