解析 nginx啓動期做了哪些事


nginx是個多進程web容器,不同的配置下它的啓動方式也是不同的,這裏我只說說最典型的啓動方式。

它有1個master進程,和多個worker進程(最優配置的數量與CPU核數相關)。那麼,首先我們要找到main函數,它在src/core/nginx.c文件中。談到源碼了,這時我們先簡單看下源碼的目錄結構吧。


nginx主要有下列目錄:

src/core,這個目錄存放了基礎的數據結構像LIST、紅黑樹、nginx字符串,貫穿始終的一些邏輯結構如ngx_cycle_s、ngx_connection_s等,還有對一些底層操作的封裝如log、文件操作、共享內存、內存池等,最後還有個nginx.c這個main啓動函數了。

src/event,這個目錄下存放與抽象事件相關的結構和鉤子函數。nginx是以事件驅動處理流程的,事件自然是整個體系的核心了,這裏定義了最核心的ngx_event_s結構。

src/event/modules目錄存放了具體的種種事件驅動方式,例如epoll、kqueue、poll、aio、select等,它們通過ngx_event_actions_t結構體中的鉤子掛在nginx中。nginx啓動時會根據配置來決定使用哪種實現方式。

src/os/unix中存放了unix系統下許多函數調用的UNIX實現。

src/http目錄存放到http module的相關實現,這個module負責處理http請求,包括協議的解析以及訪問backend server的代碼。

src/http/module目錄存放http module類型的一些特定用途的module,比如gzip處理加密,圖片壓縮等。


有個初步瞭解後,回到main函數中,順序看看我們感興趣的事情。它先執行了ngx_time_init,爲什麼要初始化時間呢?nginx考慮的還是很周到的,取系統時間gettimeofday是系統調用,這意味着,需要發送中斷給linux內核,內核需要做進程間切換來處理這個調用。這是一個不能忽視成本的函數。nginx封裝了時間函數,這樣,每次我們需要處理時間時,並不是調用gettimeofday,而是nginx自己緩存的時間,這樣大量減少了系統調用,取當前時間這事可是誰都愛乾的。


那麼,nginx是怎麼維護自己的這個時鐘呢?如何保證用戶取到的當前時間是有意義的?nginx設計者的出發點是,nginx是事件驅動機制,當一批事件發生時,也就是epoll_wait返回時,會取一次gettimeofday來更新自己的時間,然後調用各個事件對應的處理函數。這些函數都會保證自己是無阻塞的,也就是毫秒級的處理能力,所以,在任何一個事件處理函數中,取到的時間都是之前epoll_wait剛返回時取到的時間,這樣,即使拿到的時間慢了幾毫秒也無所謂。關鍵是,每個函數都是無阻塞的,都要迅速的把控制權交還給nginx,這是基本設計原則哈。


main函數初始化時間後,建立了最核心的數據結構ngx_cycle,之後無論是worker進程還是master進程都是圍繞着它進行的。下面,我們要超級關注ngx_init_cycle這個函數,啓動過程中大量的工作是在這完成的,代碼就不列了,這個函數有800行,超大,也可見其之關鍵。ngx_init_cycle裏做的第一件事就是調用所有nginx module裏的create_conf方法。好,現在我們纔來詳細看下nginx module是什麼。


nginx 抽象出一個ngx_module_s結構用來描述各個module,每個module處理它感興起的事件。nginx裏共有多少個module既是寫死在代碼中的,也是可以靈活配置的,呵呵,nginx式的玩法。回想下,下載nginx源碼包後,我們也要執行它提供的configure操作,這個命令會生成makefile和ngx_modules文件,makefilel決定編譯哪些module源文件,而生成的ngx_modules.c文件決定編譯出的執行文件究竟使用哪些module。ngx_modules.c裏面會生成一個數組ngx_modules,這是整個nginx工程都在使用的全局變量,它的形式如下:

[cpp] view plain copy
  1. ngx_module_t *ngx_modules[] = {  
  2.     &ngx_core_module,  
  3.     &ngx_errlog_module,  
  4.     &ngx_conf_module,  
  5.     ... ...  
  6. }  

這個通過configure生成的全局變量很關鍵,只有它才知道,一個請求可能會用哪些module處理。

接上文,ngx_init_cycle就是通過ngx_modules數組來調用所有module的create_conf方法的(每個module有權力決定是否實現這個方法,如果不實現的話,當然不會調用了)。然後,開始處理配置文件,這裏我們需要重點關注ngx_conf_parse函數,因爲它裏面調用了ngx_conf_handler方法,ngx_conf_handler方法會調用每個module裏自己實現的set鉤子函數,讓每個module處理自己感興趣的配置項。所以,如果你在nginx.conf裏沒有配置某個module想要的東東,這個module雖然編譯進去了,卻會一直不執行的。這裏我們要看下module的結構了,不能總是幹說哈。

[cpp] view plain copy
  1. struct ngx_module_s {  
  2.     ngx_uint_t            ctx_index;  
  3.     ngx_uint_t            index;  
  4.  ... ...  
  5.     void                 *ctx;  
  6.     ngx_command_t        *commands;  
  7.     ngx_uint_t            type;  
  8.   
  9.     ngx_int_t           (*init_master)(ngx_log_t *log);  
  10.     ngx_int_t           (*init_module)(ngx_cycle_t *cycle);  
  11.     ngx_int_t           (*init_process)(ngx_cycle_t *cycle);  
  12.     ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);  
  13.     void                (*exit_thread)(ngx_cycle_t *cycle);  
  14.     void                (*exit_process)(ngx_cycle_t *cycle);  
  15.     void                (*exit_master)(ngx_cycle_t *cycle);  
  16. ... ...  
  17. };  

ctx_index用來表示我們定義的一個module在上下文數組中的序號,index就表示在ngx_modules這個數組中的序號。

ctx這個指針指向module的上下文,type表示這個module的類型(module是分類的,每種type可以有多個module的),下面8個鉤子函數,表示對應的事件發生時會調用這些方法(當然,module也可以不實現)。commands指向這個module所屬的command結構,例如,http module是這麼定義自己的command的:

[cpp] view plain copy
  1. static ngx_command_t  ngx_http_commands[] = {  
  2.   
  3.     { ngx_string("http"),  
  4.       NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,  
  5.       ngx_http_block,  
  6.       0,  
  7.       0,  
  8.       NULL },  
  9.   
  10.       ngx_null_command  
  11. };  

我們再看看ngx_command_s的定義:

[cpp] view plain copy
  1. struct ngx_command_s {  
  2.     ngx_str_t             name;  
  3.     ngx_uint_t            type;  
  4.     char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);  
  5.     ngx_uint_t            conf;  
  6.     ngx_uint_t            offset;  
  7.     void                 *post;  
  8. };  

所以,上文我說過,ngx_conf_handler方法會調用每個module裏自己實現的set鉤子函數,如果我們編譯了http module(默認都有),那麼就會在ngx_conf_handler方法中調用上面的ngx_http_block函數。這個ngx_http_block函數值得詳細說說,因爲它這時讀取配置文件,決定要監聽哪些http端口,它會把這些信息通過傳進來的ngx_conf_t指針塞給ngx_cycle這個核心變量。

ngx_event_core_module也是個核心module,之前說到的到底是由epoll、select、poll還是kqueue來實現IO多路複用,就是由這個module來搞定的。


繼續向下。ngx_init_cycle函數再來調用所有module實現的init_conf鉤子函數。之後,執行到現在nginx進程終於要開始監聽端口了。這事很關鍵,剛剛調用過各個module的set鉤子方法了,例如上面http module的ngx_http_block方法,這些方法已經給ngx_cycle的listening數組塞進了需要監聽的端口。爲什麼要現在就開始listen呢?因爲現在還沒有fork出worker子進程哈。大家知道,linux系統下,fork出的子進程會共享父進程的地址空間,所以,需要在全部worker進程中做的事,就都放到ngx_init_cycle裏來做吧。監聽的句柄,會被所有nginx worker共享使用的。

監聽完指定的端口後,開始調用所有module實現的init_module鉤子函數。接下來,要準備進程間通訊的事了。一個master,多個worker,這些進程間通過什麼方式通訊呢?這裏不展開了,下次再細說。它們也通過共享內存交換數據,這時開始初始化共享內存。終於,ngx_init_cycle執行完了,鬆口氣?


接着,main函數要初始化信號量,進程間的同步都是通過信號量來玩的。然後創建pidfile,這個文件用於啓動完成以後通過nginx命令行,對nginx進程發送信號量來控制它。main函數的最後,開始執行ngx_master_process_cycle函數了,這個函數做master進程該做的事。它首先調用ngx_start_worker_processes去啓動worker,按照配置文件中配置的worker數量,fork出許多子進程,每個子進程執行ngx_worker_process_cycle函數,這是個死循環函數,將開始處理真正的用戶請求。

ngx_master_process_cycle函數再調用ngx_start_cache_manager_processes啓動cache的管理進程,這塊限於篇幅,下次有空再講吧。最後,ngx_master_process_cycle進入死循環,開始準備接收信號量傳來的命令,以及監控每一個worker的運行狀態,如果有worker非正常死掉,還會重新拉起的。


最後聲明下,我是以nginx-0.7.65版本做例子來說的,上面列到的源代碼文件,都是針對這個版本的。當然,我說的這些都是核心函數,其實1.x版本與之差別非常小。


熟悉了nginx的啓動過程,知道它幹了哪些事,就可以研究worker進程如何處理用戶請求了。下次再說吧

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