Mongoose源碼剖析:mongoose的工作模型

引言

我看一個項目的時候,比較喜歡首先看它的架構和設計。因爲這樣在研讀源碼的時候,有一個指導作用,不會迷失於具體細節,並能夠引導我如何去將點串成線,將線串成面。而且一個軟件怎麼樣,很大程度上取決於它採用的架構。

本文主要介紹Mongoose的工作模型,及根據這個模型將代碼大致串起來,找出主線。內容框架如下:

  • 1、線程模型
  • 2、從程序入口着手
  • 3、Mongoose的生命旅程

1、線程模型

Mongoose採用了一個自適應的線程池的模型。有一個主線程(master thread)用於打開配置端口和等待連接的到了。一旦新的連接到來,主線程將衍生一個新的線程去服務該連接。當衍生的線程處理完連接的請求之後,它會保持一段時間的空閒(可以通過配置選項-idle_time <seconds>控制空閒時間),在此期間主線程可能會傳遞另一個連接給它,讓它服務。

因此,每個連接都是在自己的線程中執行,且線程數量隨着web服務器的負載而變化。然而,最大的活躍線程數由-max_thread <number>控制。如果一旦總的線程數達到了這個閾值,當新的連接到來時,主線程將等到有線程空閒時在分配線程服務新到來的連接。以此同時,建立了TCP監聽隊列,即當沒有線程空閒時到來的新連接會被置入該隊列,當有線程空閒了會從隊列中取出連接並服務。如果沒有線程變空閒,而TCP隊列又滿了,web服務器將拒絕新到來的連接請求。

上面所述的過程大致如下所示:

image

圖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()被調用。

image

圖2、Mongoose的大概生命主線

4、總結

至此,算是介紹完了Mongoose的一個完整的工作模型了,你可以安裝此主線去進行code review。只有你腦海裏有這樣一個模型,你就不會在研讀代碼是迷失了。

當然Mongoose提供的很多api,這裏都沒有介紹到,因爲它不是本文的重點。我希望此能夠帶後來者步入Mongoose源碼研讀的大門,給後來者節省徘徊在門外停滯不前的時間。


作者:吳秦
出處:http://www.cnblogs.com/skynet/
本文基於署名 2.5 中國大陸許可協議發佈,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名吳秦(包含鏈接).


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章